---
title: "Minting user tokens — JWT for Laravel | Roundly"
description: "Mint RS256 access tokens with the fluent AccessTokenRequest, plus challenge, email-verify and custom-scope tokens."
url: https://roundly-consulting.com/open-source/docs/jwt-for-laravel/user-tokens
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[JWT for Laravel](https://roundly-consulting.com/open-source/docs/jwt-for-laravel.md)

# Minting user tokens

User tokens are RS256 JWTs signed on the issuer app with the RSA private key. The issuer pins iss, aud, sub, iat, nbf, exp, jti and scope — these always overwrite a caller-supplied claim of the same name, so extra claims can never spoof identity.

## Access tokens

AccessTokenRequest is an immutable, fluent builder — every method returns a new instance. The resulting token has scope = access:

```php
use RoundlyConsulting\Jwt\Facades\Jwt;
use RoundlyConsulting\Jwt\UserTokens\AccessTokenRequest;

$request = AccessTokenRequest::for($user->id)                     // subject (string|int)
    ->email($user->email, verified: $user->hasVerifiedEmail())    // email + email_verified claims
    ->tokenVersion($user->token_version)                          // tv claim (freshness)
    ->permissions('posts.view', 'posts.edit')                     // permissions claim
    ->withClaims(['org' => 42]);                                  // arbitrary extra claims

$issued = Jwt::mintAccessToken($request);
```

- for($subject) — the sub claim; accepts a string or an int.
- email($email, verified: bool) — the email and email\_verified claims.
- tokenVersion(int) — the tv claim compared by the guard’s token-version check.
- permissions(...$abilities) — the permissions claim read by claim-based authorization.
- withClaims(array) — arbitrary extra claims; registered and known claims always win.

## Returning the token

Every mint returns a readonly IssuedToken:

```php
$issued->token;      // string — the compact JWS to return to the client
$issued->expiresAt;  // CarbonImmutable — the expiry
$issued->jti;        // string — the token id (store it to denylist later)

return response()->json([
    'access_token' => $issued->token,
    'token_type' => 'Bearer',
    'expires_at' => $issued->expiresAt->toIso8601String(),
]);
```

## Audience, TTL and session claims

audience() and ttl() steer the issuer and are never written into the payload. sid, amr and auth\_time are emitted only when you set them, so existing call sites mint exactly what they always did:

```php
AccessTokenRequest::for($user->id)
    ->audience('app-clients')                 // aud — defaults to jwt.audience
    ->ttl(600)                                // seconds — defaults to jwt.ttl
    ->sessionId($familyId)                    // sid
    ->authMethods('pwd', 'otp', 'mfa')        // amr (RFC 8176)
    ->authTime($loggedInAt);                  // auth_time — int or any DateTimeInterface

$claims->sessionId();    // ?string
$claims->authMethods();  // list<string>, [] when absent
$claims->authTime();     // ?int
```

ttl() below one second, an empty sessionId() and an empty or missing authMethods() throw InvalidArgumentException. amr is de-duplicated, first occurrence wins.

## Challenge, email-verify and custom scopes

The 2fa\_pending and email\_verify mints use the configured challenge\_ttl and verify\_ttl — no hand-passing the number:

```php
Jwt::mintChallengeToken($subject);                 // scope=2fa_pending, exp = challenge_ttl
Jwt::mintEmailVerifyToken($subject, $user->email); // scope=email_verify, exp = verify_ttl

// Extra claims on a challenge token:
Jwt::mintChallengeToken($subject, ['method' => 'totp']);

// Any other scope via the generic mint():
Jwt::mint($subject, 'my_custom_scope', ttl: 120, extraClaims: ['org' => 42]);
```

## The Scope enum

The four built-in scopes are a backed enum. mint() accepts a Scope case or any string, so custom scopes stay free strings:

```php
use RoundlyConsulting\Jwt\UserTokens\Scope;

Scope::Access->value;        // 'access'
Scope::TwoFaPending->value;  // '2fa_pending'
Scope::EmailVerify->value;   // 'email_verify'
Scope::Service->value;       // 'service'

Jwt::mint($subject, Scope::Access, ttl: 900);  // pass an enum case, or any string
Scope::values();                               // Collection: ['access', '2fa_pending', …]
```

## Without the facade

Prefer the facade. Under the hood it delegates to the UserTokenIssuer contract (bound to NativeUserTokenIssuer), which you can also resolve directly — see DI and actions:

```php
use RoundlyConsulting\Jwt\UserTokens\AccessTokenRequest;
use RoundlyConsulting\Jwt\UserTokens\Contracts\UserTokenIssuer;

$issued = app(UserTokenIssuer::class)->mintAccessToken(
    AccessTokenRequest::for($user->id)->email($user->email, verified: true)
);
```

## Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

[More ways to support, including crypto](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/jwt-for-laravel.md)
