Skip to main content
Token issuance in this package is intentionally separated from the guard’s bearer-authentication path. You call the issuer explicitly at login time — typically inside a controller action that has already validated credentials — and return the resulting strings to the client. The guard itself handles verification on every subsequent request.
Access tokens are self-verifying JWTs — there is no server-side access-token store and no revocation list the guard consults. On the bearer path, the guard verifies the signature and claims, then rehydrates the identity and principal from the database. Revoking a device blocks future refresh exchanges, but access tokens already in flight remain valid until their exp claim is reached.

Get the JWT token service

Access the guard-scoped token service through the Auth facade. Pass the guard name, or omit it to use the default guard:
use SineMacula\Laravel\Authentication\Facades\Auth;

$tokens = Auth::jwt('api');   // Guard-scoped JwtTokenService for the 'api' guard
$tokens = Auth::jwt();        // Uses the default guard
The returned JwtTokenService is already configured with the signing material, algorithm, TTLs, issuer, and audience for that specific guard.

Issue an access token

Call issueAccessToken() with the authenticated identity, the acting principal, and the device. In 2D mode the identity and principal are the same model instance. Pass null for the device if you are running in access-only mode without device tracking.
$accessToken = Auth::jwt('api')->issueAccessToken($identity, $principal, $device);
The three parameters map directly to claims embedded in the token:
ParameterClaim embeddedNotes
$identitysubThe identity’s auth identifier (e.g. UUID or integer primary key)
$principalpidThe principal’s stable identifier from getPrincipalIdentifier()
$devicedidThe device’s identifier, or null in access-only mode
Additional claims written automatically: jti (random token id), iat (issued-at), exp (expiry), typ: "access", and optionally iss and aud if your guard configuration includes them.

Issue a refresh token

Call issueRefreshToken() on the guard or its token service after issuing the access token. You must generate a plaintext rotation id, embed it in the refresh JWT, and store its hash on the device row — never the plaintext value.
use SineMacula\Laravel\Authentication\Jwt\RefreshTokenHasher;

$rotationId   = RefreshTokenHasher::generate();         // plaintext — embed in JWT
$rotationHash = RefreshTokenHasher::hash($rotationId);  // hash — persist on device row

$device->update(['refresh_key' => $rotationHash]);

$refreshToken = Auth::jwt('api')->issueRefreshToken($device, $rotationId, $principal);
The $principal parameter is optional. When provided, the refresh token carries a pid hint that the guard validates during rotation — the resolved principal must match the hint or the exchange fails closed. Refresh token claims: did (device identifier), jti (the plaintext rotation id), iat, exp, typ: "refresh", and optionally pid, iss, aud.

Typical login response

A standard login controller issues both tokens and returns them to the client in a single response:
use App\Models\AppUser;
use Illuminate\Http\Request;
use SineMacula\Laravel\Authentication\Facades\Auth;
use SineMacula\Laravel\Authentication\Jwt\RefreshTokenHasher;
use SineMacula\Laravel\Authentication\Models\Device;

class LoginController
{
    public function __invoke(Request $request): array
    {
        $identity = AppUser::where('email', $request->email)->firstOrFail();

        if (!Hash::check($request->password, $identity->password)) {
            abort(401);
        }

        $principal  = $identity; // 2D: identity is the principal
        $rotationId = RefreshTokenHasher::generate();

        $device = $identity->devices()->create([
            'refresh_key' => RefreshTokenHasher::hash($rotationId),
            'os'          => $request->userAgent(),
        ]);

        $accessToken  = Auth::jwt('api')->issueAccessToken($identity, $principal, $device);
        $refreshToken = Auth::jwt('api')->issueRefreshToken($device, $rotationId, $principal);

        return [
            'access_token'  => $accessToken,
            'refresh_token' => $refreshToken,
        ];
    }
}

Access-only mode

If your app does not need device tracking or refresh rotation — common for M2M APIs and short-lived session flows — skip the devices migration and pass null as the device:
$accessToken = Auth::jwt('api')->issueAccessToken($identity, $principal, null);
The token carries did: null. On the bearer path, Auth::device() returns null. The full identity/principal context (Auth::identity(), Auth::principal(), Auth::tenant(), Auth::type()) still works normally. You can add device tracking and refresh later — it is purely additive.