> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sinemacula.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Issue JWT Access and Refresh Tokens

> Call issueAccessToken and issueRefreshToken on the guard-scoped JwtTokenService to mint tokens for authenticated users and devices.

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.

<Note>
  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.
</Note>

## 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:

```php theme={null}
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.

```php theme={null}
$accessToken = Auth::jwt('api')->issueAccessToken($identity, $principal, $device);
```

The three parameters map directly to claims embedded in the token:

| Parameter    | Claim embedded | Notes                                                             |
| ------------ | -------------- | ----------------------------------------------------------------- |
| `$identity`  | `sub`          | The identity's auth identifier (e.g. UUID or integer primary key) |
| `$principal` | `pid`          | The principal's stable identifier from `getPrincipalIdentifier()` |
| `$device`    | `did`          | The 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.

```php theme={null}
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:

```php theme={null}
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:

```php theme={null}
$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.
