Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Radius Laravel Package

dapphp/radius

Pure PHP RADIUS client for authentication, authorization, and accounting. Send Access-Request, Accounting-Request, and CoA/Disconnect packets, with support for vendor-specific attributes, retries, timeouts, and easy integration into any PHP or Laravel application.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and inspect

composer require dapphp/radius

The package is a pure-PHP UDP client — no PECL extension or daemon needed for PAP authentication. Before writing code, read in this order:

  1. README.md — the canonical PAP/MSCHAP examples.
  2. src/Radius.php — the convenience API (accessRequest(), NAS setters, attribute helpers, timeout/error handling).
  3. src/RadiusAccessRequest.php and its subclasses (...PAP, ...CHAP, ...MSCHAPv1, ...MSCHAPv2) — the object-oriented request path you'll need for anything beyond PAP.
  4. src/RadiusResponse.php / src/RadiusPacket.php — how attributes are read back (Framed-IP, Filter-Id, Class, Reply-Message).

Minimal working call

use Dapphp\Radius\Radius;

$radius = new Radius('10.0.0.10', 'shared-secret', 3, 1812);
$radius->setNasIpAddress('10.0.0.5');   // usually required by the server
$radius->setNasPort(0);

if ($radius->accessRequest($username, $password)) {
    // Access-Accept
} else {
    // Access-Reject *or* a transport/config error — always inspect the message
    logger()->warning('RADIUS failure: ' . $radius->getErrorMessage());
}

First use case

Centralizing credential checks that used to live in users.password — a VPN portal, an admin console, a Wi-Fi captive portal, or a legacy app that must authenticate against FreeRADIUS, Windows NPS, or Cisco ISE. The 80% path is: configure host/secret, set NAS-IP-Address, call accessRequest(), branch on the boolean, and log the error string.

Implementation Patterns

1. Wrap it in a single service, never call it inline

RADIUS is network I/O with a shared secret; it deserves a class and a config entry.

// config/radius.php
return [
    'host'    => env('RADIUS_HOST', '127.0.0.1'),
    'secret'  => env('RADIUS_SECRET'),
    'port'    => (int) env('RADIUS_PORT', 1812),
    'timeout' => (int) env('RADIUS_TIMEOUT', 3),
    'nas_ip'  => env('RADIUS_NAS_IP'),
];

// app/Services/RadiusAuthenticator.php
class RadiusAuthenticator
{
    public function authenticate(string $user, string $pass, array $extra = []): RadiusResult
    {
        $radius = new Radius(config('radius.host'), config('radius.secret'),
                             config('radius.timeout'), config('radius.port'));
        $radius->setNasIpAddress(config('radius.nas_ip'));

        foreach ($extra as $type => $value) {
            $radius->setAttribute($type, $value);
        }

        $ok = $radius->accessRequest($user, $pass);

        return new RadiusResult($ok, $radius->getErrorMessage());
    }
}

Bind it as a singleton/interface so tests can swap in a fake.

2. Custom auth guard / user provider

The cleanest Laravel integration is a retrieveByCredentials() that calls your wrapper instead of Hash::check(). Keep local authorization (roles, tenancy) in the database keyed by username — RADIUS answers "is this password valid", not "what may this user do".

public function retrieveByCredentials(array $credentials)
{
    $user = User::where('email', $credentials['email'])->first();
    if (! $user) { return null; }

    return $this->radius->authenticate($credentials['email'], $credentials['password'])->accepted
        ? $user
        : null;
}

3. Use the request objects when you need more than PAP

For MSCHAPv2 (common with NPS/AD), build the explicit request and keep the response object — it gives you attribute access and accept/reject/challenge discrimination.

$request = new RadiusAccessRequestMSCHAPv2($radius);
$request->setUsername($username);
$request->setPassword($plaintextPassword);
$response = $request->sendRequest();

if ($response->isAccessAccept()) {
    $framedIp = $response->getAttribute(8);         // Framed-IP-Address
    $filterId = $response->getAttribute(11);        // Filter-Id
    $class    = $response->getAttribute(25);        // Class (echo back on accounting)
}

4. Handle Access-Challenge explicitly

Some servers (OTP, MFA, password-expiry flows) reply with Access-Challenge + State. Treat that as a distinct third outcome and re-send with the State attribute echoed back rather than collapsing it into "rejected".

5. Vendor-specific attributes

Use the VendorSpecificAttribute helper rather than hand-packing bytes when you need WISPr, Cisco AV-pairs, or MikroTik rate limits. The encoding is vendor-id / vendor-type / length / value, and getting the length or type width wrong produces a server-side parse error that looks like a silent reject.

6. Timeouts and retry budget

Set the socket timeout short (2–3 s) in HTTP requests and treat retries as a bounded budget. For bulk operations (importing users, nightly provisioning checks) push the work onto a queue — a web request that blocks on three UDP retries is a request that holds a PHP-FPM worker.

7. Local development and tests

Run FreeRADIUS in Docker with a users file and start it with radiusd -X to see every packet in the foreground. Point your .env at it. For unit tests, mock your RadiusAuthenticator interface; keep one integration test that hits the container so protocol regressions are caught.

Gotchas and Tips

  • Secret mismatch is not an auth failure. A wrong shared secret generally manifests as a timeout or a response-authenticator failure, not a Reject. If every request fails identically, check the secret before checking credentials.
  • Set NAS-IP-Address. Many servers require it and will silently drop requests without it; also make sure the source IP of your app is listed in the server's clients.conf.
  • The return value doesn't distinguish reject from error. accessRequest() returns false for both. Always log/read getErrorMessage(); otherwise a down server looks like "wrong password".
  • UDP + blocking = latency. Default timeouts and retries are generous for a CLI and painful for HTTP. Lower the timeout and understand that the call blocks the worker.
  • PAP password length. RFC 2865 limits the encrypted password to 128 octets; very long passwords, multibyte UTF-8, or binary data need scrutiny. Normalize/validate input before sending.
  • MSCHAPv1/v2 need the plaintext password and depend on PHP's crypto primitives (DES via OpenSSL / hash). Verify the extension is present in your runtime, and that you're on 64-bit PHP — these paths are where "works locally, fails in the container" usually shows up.
  • Don't reuse a Radius instance across requests without care. Request identifiers increment and attributes you set persist; a fresh instance per authentication is the safest default.
  • Attribute typing matters. Strings vs integers vs IP addresses are encoded differently. Passing "10.0.0.1" where an ipaddr is expected (or vice versa) yields cryptic server behavior.
  • Message-Authenticator / BlastRADIUS hardening. Post-2024, many servers require the Message-Authenticator attribute on Access-Requests. If requests suddenly stop being answered after a server upgrade, this is the first thing to check; you may need to add the attribute manually or upgrade the library.
  • Accounting isn't the primary focus. The API is built around access requests. If you need Acct-Status-Start/Stop, confirm current support in the source first; plan to build on RadiusPacket/RadiusRequest if it isn't first-class.
  • Debugging technique. Compare what the library sends against radclient or tcpdump -i any -n port 1812 -X; also check for the classic port mistake (1812 vs legacy 1645).
  • Never log the shared secret or the plaintext password, and keep the secret in env()/a secrets manager — the constructor takes it as a string, so it will end up in stack traces if you're careless.
  • License. LGPL-3.0: fine to depend on from a proprietary Laravel app, but if you patch the library itself, those changes should be made available.
  • Pin the version. The package has a long history and PHP 8 compatibility landed across releases; lock a known-good version in composer.json and test upgrades against your RADIUS server, not just the unit suite.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle