Integrating with Laravel API
This guide explains how to implement JWT authentication in your Laravel API using AuthAction’s JWKS (JSON Web Key Set) endpoint via a custom middleware.
Example Repository: For a complete working example, check out our example repository.
Prerequisites
Section titled “Prerequisites”Before you begin, ensure you have:
- PHP 8.2+ and Composer: Download from php.net
- AuthAction Account: You’ll need your AuthAction tenant domain and API identifier
Configuration
Section titled “Configuration”1. Install Required Packages
Section titled “1. Install Required Packages”composer require lcobucci/jwt lcobucci/clock2. Configure AuthAction Settings
Section titled “2. Configure AuthAction Settings”Create a .env file in your project root:
AUTHACTION_DOMAIN=your-authaction-tenant-domainAUTHACTION_AUDIENCE=your-authaction-api-identifier3. Add AuthAction Config
Section titled “3. Add AuthAction Config”Create config/authaction.php:
<?php
return [ 'domain' => env('AUTHACTION_DOMAIN'), 'audience' => env('AUTHACTION_AUDIENCE'),];4. Implement the JWT Middleware
Section titled “4. Implement the JWT Middleware”Create app/Http/Middleware/AuthActionJWT.php:
<?php
namespace App\Http\Middleware;
use Closure;use Illuminate\Http\Request;use Illuminate\Support\Facades\Cache;use Illuminate\Support\Facades\Http;use Lcobucci\Clock\SystemClock;use Lcobucci\JWT\Configuration;use Lcobucci\JWT\Signer\Key\InMemory;use Lcobucci\JWT\Signer\Rsa\Sha256;use Lcobucci\JWT\Token\Plain;use Lcobucci\JWT\Validation\Constraint\IssuedBy;use Lcobucci\JWT\Validation\Constraint\PermittedFor;use Lcobucci\JWT\Validation\Constraint\SignedWith;use Lcobucci\JWT\Validation\Constraint\StrictValidAt;use Lcobucci\JWT\Validation\RequiredConstraintsViolated;use Symfony\Component\HttpFoundation\Response;
class AuthActionJWT{ public function handle(Request $request, Closure $next): Response { $token = $request->bearerToken();
if (!$token) { return response()->json(['error' => 'Unauthorized: missing token'], Response::HTTP_UNAUTHORIZED); }
try { $parsed = $this->verifyToken($token); $request->attributes->set('jwt_payload', (object) $parsed->claims()->all()); } catch (\Throwable $e) { return response()->json(['error' => $e->getMessage()], Response::HTTP_UNAUTHORIZED); }
return $next($request); }
private function verifyToken(string $token): Plain { $domain = config('authaction.domain'); $audience = config('authaction.audience'); $issuer = "https://{$domain}";
// Decode header to extract kid without full validation $parts = explode('.', $token); $header = json_decode(base64_decode(strtr($parts[0] ?? '', '-_', '+/')), true) ?? []; $kid = $header['kid'] ?? null;
$pem = $this->getPublicKey($domain, $kid); $config = $this->makeConfig($pem, $issuer, $audience);
try { $parsed = $config->parser()->parse($token); } catch (\Throwable $e) { throw new \RuntimeException('Invalid token: ' . $e->getMessage()); }
assert($parsed instanceof Plain);
try { $config->validator()->assert($parsed, ...$config->validationConstraints()); } catch (RequiredConstraintsViolated) { // Possible key rotation — bust cache and retry once Cache::forget("authaction_jwks_{$domain}"); $pem = $this->getPublicKey($domain, $kid); $config = $this->makeConfig($pem, $issuer, $audience); $config->validator()->assert($parsed, ...$config->validationConstraints()); }
return $parsed; }
private function makeConfig(string $pem, string $issuer, string $audience): Configuration { $config = Configuration::forAsymmetricSigner( new Sha256(), InMemory::plainText(''), InMemory::plainText($pem) );
$config->setValidationConstraints( new SignedWith($config->signer(), $config->verificationKey()), new IssuedBy($issuer), new PermittedFor($audience), new StrictValidAt(SystemClock::fromSystemTimezone()), );
return $config; }
private function getPublicKey(string $domain, ?string $kid): string { $keys = Cache::remember("authaction_jwks_{$domain}", ttl: 3600, callback: function () use ($domain) { $response = Http::get("https://{$domain}/.well-known/jwks.json"); $response->throw(); return $response->json('keys', []); });
foreach ($keys as $key) { if ($kid === null || ($key['kid'] ?? null) === $kid) { return $this->jwkToPem($key); } }
throw new \RuntimeException('Matching public key not found'); }
// Converts a JWK (n, e components) to a PEM-encoded RSA public key // using raw ASN.1 DER encoding — no extra dependencies required. private function jwkToPem(array $jwk): string { $n = $this->base64UrlDecode($jwk['n']); $e = $this->base64UrlDecode($jwk['e']);
if (ord($n[0]) > 0x7f) $n = "\x00" . $n; if (ord($e[0]) > 0x7f) $e = "\x00" . $e;
$rsaKey = $this->asn1Seq($this->asn1Int($n) . $this->asn1Int($e)); $spki = $this->asn1Seq( $this->asn1Seq("\x06\x09\x2a\x86\x48\x86\xf7\x0d\x01\x01\x01\x05\x00") . $this->asn1BitStr("\x00" . $rsaKey) );
return "-----BEGIN PUBLIC KEY-----\n" . chunk_split(base64_encode($spki), 64, "\n") . "-----END PUBLIC KEY-----\n"; }
private function base64UrlDecode(string $data): string { return base64_decode(strtr($data, '-_', '+/') . str_repeat('=', (4 - strlen($data) % 4) % 4)); }
private function asn1Len(int $len): string { if ($len < 128) return chr($len); $bytes = ''; while ($len > 0) { $bytes = chr($len & 0xff) . $bytes; $len >>= 8; } return chr(0x80 | strlen($bytes)) . $bytes; }
private function asn1Int(string $bytes): string { return "\x02" . $this->asn1Len(strlen($bytes)) . $bytes; } private function asn1Seq(string $bytes): string { return "\x30" . $this->asn1Len(strlen($bytes)) . $bytes; } private function asn1BitStr(string $bytes): string { return "\x03" . $this->asn1Len(strlen($bytes)) . $bytes; }}5. Register the Middleware
Section titled “5. Register the Middleware”In bootstrap/app.php, register AuthActionJWT as the auth.jwt alias:
<?php
use App\Http\Middleware\AuthActionJWT;use Illuminate\Foundation\Application;use Illuminate\Foundation\Configuration\Exceptions;use Illuminate\Foundation\Configuration\Middleware;
return Application::configure(basePath: dirname(__DIR__)) ->withRouting( api: __DIR__.'/../routes/api.php', apiPrefix: '', ) ->withMiddleware(function (Middleware $middleware) { $middleware->alias([ 'auth.jwt' => AuthActionJWT::class, ]); }) ->withExceptions(function (Exceptions $exceptions) {}) ->create();1. Define Your Routes
Section titled “1. Define Your Routes”In routes/api.php:
<?php
use Illuminate\Http\Request;use Illuminate\Support\Facades\Route;
Route::get('/public', function () { return response()->json(['message' => 'This is a public message!']);});
Route::middleware('auth.jwt')->get('/protected', function (Request $request) { $payload = $request->attributes->get('jwt_payload');
return response()->json([ 'message' => 'This is a protected message!', 'sub' => $payload->sub, ]);});2. Start the Server
Section titled “2. Start the Server”php artisan serveThe API will be available at http://localhost:8000.
3. Testing the API
Section titled “3. Testing the API”- Obtain an Access Token:
curl --request POST \ --url https://your-authaction-tenant-domain/oauth2/m2m/token \ --header 'content-type: application/json' \ --data '{ "client_id": "your-authaction-m2m-app-clientid", "client_secret": "your-authaction-m2m-app-client-secret", "audience": "your-authaction-api-identifier", "grant_type": "client_credentials" }'- Call the Public Endpoint:
curl http://localhost:8000/public- Call the Protected Endpoint:
curl --request GET \ --url http://localhost:8000/protected \ --header 'Authorization: Bearer YOUR_ACCESS_TOKEN'Common Issues
Section titled “Common Issues”Invalid Token Errors
Section titled “Invalid Token Errors”- Verify
AUTHACTION_DOMAINandAUTHACTION_AUDIENCEmatch your AuthAction dashboard exactly - Ensure the token is signed with the RS256 algorithm
Public Key Fetching Errors
Section titled “Public Key Fetching Errors”- Verify your application can reach
https://your-authaction-tenant-domain/.well-known/jwks.json
Unauthorized Access
Section titled “Unauthorized Access”- Ensure the
Authorization: Bearer <token>header is present and the token is not expired - Confirm the token’s audience matches your API identifier