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

Duo Universal Php Laravel Package

duosecurity/duo_universal_php

Official Duo Security PHP SDK for the Universal Prompt. Add two-factor authentication to PHP apps with OAuth 2.0 token exchange, redirect handling, and callback verification against Duo's service.

Deep Wiki
Context7

Getting Started

composer require duosecurity/duo_universal_php

Before writing code, create a Web SDK application in the Duo Admin Panel. That gives you three values you cannot derive from anywhere else: the Client ID, the Client Secret, and the API hostname (api-xxxxxxxx.duosecurity.com). Register your callback URL there too — it must match byte-for-byte what you pass to the SDK.

// config/services.php
'duo' => [
    'client_id'     => env('DUO_CLIENT_ID'),
    'client_secret' => env('DUO_CLIENT_SECRET'),
    'api_host'      => env('DUO_API_HOST'),      // hostname only, no https://
    'redirect_uri'  => env('DUO_REDIRECT_URI'),  // full absolute URL
],
use Duo\DuoUniversal\Client;

$duo = new Client(
    config('services.duo.client_id'),
    config('services.duo.client_secret'),
    config('services.duo.api_host'),
    config('services.duo.redirect_uri'),
);

$duo->healthCheck(); // throws DuoException on bad creds/host/network

Where to look first: src/Client.php in the package (it is small — read it end to end) and the examples/ directory in the repo. The class exposes essentially four things you care about: healthCheck(), generateState(), createAuthUrl($username, $state), and exchangeAuthorizationCodeFor2FAResult($code, $state). Everything else in the SDK is JWT verification and cURL plumbing you don't touch.

First use case: gate a single high-value route (admin panel, billing settings) behind Duo rather than retrofitting the whole login flow. That gets you a working state → prompt → callback → session loop in an afternoon with a small blast radius.

Implementation Patterns

Two-phase controller flow. The prompt and the callback are separate requests. Keep state in the session between them.

Route::get('/duo/prompt',   [DuoController::class, 'prompt'])->name('duo.prompt');
Route::get('/duo/callback', [DuoController::class, 'callback'])->name('duo.callback');
public function prompt(Request $request)
{
    $state = $this->duo->generateState();

    $request->session()->put('duo.state', $state);
    $request->session()->put('duo.intended', url()->previous());
    $request->session()->put('duo.started_at', now());

    return redirect(
        $this->duo->createAuthUrl($request->user()->email, $state)
    );
}

public function callback(Request $request)
{
    $expected = $request->session()->pull('duo.state');

    if (! $expected || ! hash_equals($expected, (string) $request->query('state'))) {
        abort(400, 'Invalid 2FA state');
    }

    try {
        $result = $this->duo->exchangeAuthorizationCodeFor2FAResult(
            $request->query('code'),
            $expected,
        );
    } catch (DuoException $e) {
        report($e);
        return redirect()->route('login')->withErrors('Two-factor check failed.');
    }

    $request->session()->put('duo.verified_user', $result->getUsername());
    $request->session()->put('duo.verified_at', now());

    return redirect($request->session()->pull('duo.intended', '/'));
}

Wrap the SDK behind your own interface. The Client is a concrete class that talks over cURL, so you cannot inject a mocked HTTP handler in tests. Bind your own TwoFactorProvider contract in a service provider, implement it with Duo\DuoUniversal\Client, and swap in a fake for feature tests and local development. This is the single highest-leverage thing you can do on day one.

Treat the result as an assertion, not an identity source. getUsername() tells you who Duo authenticated; it should match the user you already have in session. Compare them and reject on mismatch — that closes the "user A starts the prompt, user B finishes it" window.

Add a freshness window. Store duo.verified_at and re-prompt after N hours (or on privilege escalation). Middleware is the natural place:

if (! $request->session()->has('duo.verified_at')
    || $request->session()->get('duo.verified_at')->lt(now()->subHours(8))) {
    return redirect()->route('duo.prompt');
}

Health check on a schedule, not per request. healthCheck() is a live HTTP round trip. Put it in an Artisan command run by the scheduler, or expose it through a /health endpoint your uptime monitor hits. Doing it in a middleware adds latency to every request and hammers Duo's API.

Fail closed by default, with a documented break-glass. If healthCheck() fails or the callback throws, deny access and alert. If you need a fail-open path for staff during a Duo outage, make it an explicit, logged, role-gated escape hatch — never a silent catch that lets the request through.

Gotchas and Tips

  • api_host is a hostname, not a URL. https://api-xxxx.duosecurity.com silently produces a confusing cURL/connection error. Strip the scheme.
  • redirect_uri mismatches surface late. A trailing slash or http vs https difference usually doesn't break createAuthUrl() — it breaks the token exchange on the callback, so you get an error after the user has already completed the Duo prompt. Verify it in a smoke test against your real environment.
  • State must be single-use and per-attempt. pull() it, don't get() it. If a user opens two tabs you'll clobber a single duo.state key; namespace the key by attempt ID in the URL if that matters for your flow.
  • Use hash_equals when comparing state, and compare it yourself even though the SDK cross-checks it. Defense in depth costs one line.
  • Username format matters more than anything else in this integration. The string you pass to createAuthUrl() must resolve to a Duo user — email vs. username depends on your Duo directory configuration. Aliases configured in the Duo Admin Panel are your fix when it's wrong; there's no SDK-side mapping.
  • Clock skew causes intermittent, hard-to-reproduce failures. ID token validation checks iat/exp. Containers with drifting clocks will fail a small percentage of logins. Keep NTP healthy and don't write off sporadic DuoExceptions as noise.
  • Catch DuoException specifically, not \Exception. The SDK throws it for config errors, network failures, and invalid tokens alike — log the message, since it's usually descriptive enough to diagnose without a debugger.
  • There is no group/policy introspection. The SDK tells you success or failure for one username. Per-group enforcement (e.g., "only finance requires 2FA") has to live in your application, based on data you already have.
  • Never log the client secret. It sits in the Client instance; Laravel's report() on a DuoException won't leak it, but a careless dd($client) or a config dump in an error page will.
  • The SDK is not a general OIDC client. It won't hand you an access token or arbitrary claims for use elsewhere. If you later need "login with Duo," that's a different integration — don't try to grow this one.
  • Pin the version. This package is vendor-maintained but low-traffic on GitHub; read the changelog before bumping composer.json, and check the PHP version constraint against your runtime rather than assuming ^1.0 covers your PHP 8.x build.
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.
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log