Customization
lukk follows the Sanctum pattern: every moving part is either a contract bound to a default (rebind it in a service provider) or a closure hook on the static Lukk class (register it from a service provider's boot method). You never edit the package. This page is server-focused.
The Lukk hub
The Lukk class is a static configuration hub, like Sanctum. Register hooks from the boot method of a service provider (for example App\Providers\AppServiceProvider):
use Lukk\Lukk;
public function boot(): void
{
Lukk::authenticateUsing(/* ... */);
Lukk::tokenClaimsUsing(/* ... */);
Lukk::useRefreshTokenModel(/* ... */);
}Lukk::actingAs() (for authenticating a user in your own tests) and Lukk::disableScheduling() (to take over the lukk:prune cadence — see Deployment) live on the same hub.
Custom login logic
By default lukk validates the email and password against your configured user provider. To take full control — extra conditions, a different credential field, a "must be active" check — pass a closure to authenticateUsing. Return the authenticated user, or null to reject:
use Illuminate\Http\Request;
use Lukk\Lukk;
Lukk::authenticateUsing(function (Request $request) {
$user = User::where('email', $request->input('email'))->first();
if ($user && Hash::check($request->input('password'), $user->password) && $user->is_active) {
return $user;
}
return null;
});The login throttle still wraps your closure — failed attempts are rate-limited exactly as on the default path. Constant-time behaviour, however, becomes your responsibility: the package's unknown-user timing equalizer only runs on the built-in email/password path, so a closure that does User::where(...)->first() and hashes only when the user exists leaks a user-enumeration timing oracle. Make your closure take the same time whether or not the account exists — e.g. always run a Hash::check against a dummy hash when no user is found.
Custom token claims
Add custom claims — roles, a tenant id, anything your API needs — to every access token. The closure receives the user id and returns an array of claims:
use Lukk\Lukk;
Lukk::tokenClaimsUsing(fn ($userId) => [
'roles' => User::find($userId)->roles->pluck('name'),
]);NOTE
Your claims are merged in, but the standard claims (sub, exp, iss, aud, jti, fid, …) always win and cannot be overridden.
Swapping the refresh token model
To use your own Eloquent model for refresh tokens (to add columns, relationships, or scopes), extend the base model and register it — the Sanctum approach:
use Lukk\Lukk;
use App\Models\RefreshToken;
Lukk::useRefreshTokenModel(RefreshToken::class);Swapping storage
Refresh-token storage sits behind Contracts\RefreshTokenRepository, separate from the rotation policy (which lives in Actions\RotateRefreshToken). To move storage from the database to Redis, bind your own implementation — the policy is untouched:
use Lukk\Contracts\RefreshTokenRepository;
use App\Auth\RedisRefreshTokenRepository;
$this->app->bind(RefreshTokenRepository::class, RedisRefreshTokenRepository::class);Reshaping responses
The login, refresh, and logout responses are Responsable contracts. Rebind one to change the body shape, add headers, or switch between JSON and cookies:
use Lukk\Contracts\LoginResponse;
use App\Http\Responses\MyLoginResponse;
$this->app->bind(LoginResponse::class, MyLoginResponse::class);The response contracts are LoginResponse, RefreshResponse, LogoutResponse, and TwoFactorChallengeResponse.
NOTE
The default response shape is the contract the lukk-js clients consume. If you reshape it, keep the client in sync (or adapt it) so the two don't drift — see Authentication and Using lukk-core.
Swapping the issuer, verifier, or denylist
The cryptographic and revocation seams are contracts too. Rebind Contracts\TokenIssuer or Contracts\TokenVerifier to change how tokens are minted or validated (for example to move to RS256 — though that's built in; see Deployment → Asymmetric keys), or Contracts\Denylist to back revocation with something other than the cache.
Available contracts
| Contract | Default | Responsibility |
|---|---|---|
TokenIssuer | FirebaseTokenIssuer | Mints access tokens. |
TokenVerifier | FirebaseTokenVerifier | Verifies access tokens and checks the denylist. |
RefreshTokenRepository | DatabaseRefreshTokenRepository | Persists refresh tokens and families. |
Denylist | CacheDenylist | Records and checks revoked jti/fid values. |
LoginResponse / RefreshResponse / LogoutResponse | built-in | Shape the HTTP responses. |
TwoFactorChallengeResponse | built-in | Shapes the 2FA login challenge. |
TwoFactorProvider | Google2FaTotpProvider | Generates and verifies TOTP codes. |
WebAuthnCeremony | SpomkyWebAuthnCeremony | Performs WebAuthn registration/assertion. |
PasskeyRepository | DatabasePasskeyRepository | Persists passkey credentials. |
That's the whole customization surface. For the design rationale behind these seams, see Architecture; for the events lukk fires at the security-relevant moments, see Events. Questions or contributions are welcome on the lukk and lukk-js repositories.