Authentication

Laravel Restify provides comprehensive authentication with Laravel Sanctum, including register, login, logout, forgot password, reset password, and email verification.

Quick start

This is everything you need to have authentication in place:

If you run on Laravel 11 or higher, use this single command for complete authentication setup:

php artisan restify:setup-auth

This command will:

  • Ensures that Sanctum is installed and configured as the authentication provider in the config/restify.php file
  • Adds the HasApiTokens trait to your User model, if it's missing
  • Appends the Route::restifyAuth(); line to the routes/api.php file to add the authentication routes

Under the hood, the route-append step is its own command, php artisan restify:auth-macro, which you can also run on its own if you only need the routes/api.php line appended.

That's it! Your authentication system is ready to use with register, login, logout, password reset, and email verification.


Prefer manual setup? You can configure everything step-by-step as described below, starting with the Install sanctum section.

Install sanctum

Laravel 11 automatically ships with Sanctum, so you don't have to install it.

See the documentation here. You don't need to add \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class, to your 'api' middleware group.

You only have to run these 3 commands:

composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate

Define auth model

Define your authenticatable class in the config file:

PHP config/restify.php
'auth' => [
    ...
   'user_model' => \App\Models\User::class,
]

The User model should extend the Illuminate\Foundation\Auth\User class or implement the Illuminate\Contracts\Auth\Authenticatable interface.

Make sure you have the \Laravel\Sanctum\HasApiTokens trait to your User model. Laravel 11 will automatically add this trait to your User model.

PHP User.php
use Illuminate\Foundation\Auth\User as Authenticatable;
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens;

Define routes

Restify provides you a simple way to add all of your auth routes prepared. Simply add in your routes/api.php:

Route::restifyAuth();

And that's it! You now have authentication routes ready to use.

These are the default routes provided by restify:

VerbURIAction
POST/api/registerregister
POST/api/loginlogin
POST/api/logoutlogout
POST/api/restify/forgotPasswordforgotPassword
POST/api/restify/resetPasswordresetPassword
POST/api/restify/verify/{id}/{emailHash}verifyEmail

The register and login routes are outside the base restify prefix because they don't have to follow the auth middleware defined in the config/restify.php config file.

You can also pass an actions argument, which is an array of actions you want to register. For example:

Route::restifyAuth(actions: ['login', 'register']);

When using the actions argument, only the specified routes will be registered. If no actions argument is provided, Restify will register all routes by default.

Sanctum Middleware

Next, add the auth:sanctum middleware after the api middleware in your config file to protect all the restify's routes:

/config/restify.php
    'middleware' => [
        'api',
        'auth:sanctum',
        ...
    ],

Login

Let's ensure the authentication is working correctly. Create a user in the DatabaseSeeder class:

PHP DatabaseSeeder.php
\App\Models\User::factory()->create([
   'name' => 'Test User',
   'email' => '[email protected]',
   'password' => \Illuminate\Support\Facades\Hash::make('password'),
]);

Seed it:

php artisan db:seed

Now you can test the login with Curl or Postman:

curl -X POST "http://restify-app.test/api/login" \
     -H "Accept: application/json" \
     -H "Content-Type: application/json" \
     -d '{
             "email": "[email protected]",
             "password": "password"
         }'

You should see a response like this:

{
    "id": "11",
    "type": "users",
    "attributes": {
        "name": "Test User",
        "email": "[email protected]"
    },
    "meta": {
        "authorizedToShow": true,
        "authorizedToStore": false,
        "authorizedToUpdate": false,
        "authorizedToDelete": false,
        "token": "1|f7D1qkALtM9GKDkjREKpwMRKTZg2ZnFqDZTSe53k"
    }
}

Authorization

We will discuss authorization in more detail in the Authorization section. For now, let's see a simple example.

After a successful login, you will receive an authentication token. You should include this token as a Bearer token in the Authorization header for your subsequent API requests using Postman, axios library, or cURL.

Here's an Axios example for retrieving the user's profile with the generated token:

import axios from 'axios';

const token = '1|f7D1qkALtM9GKDkjREKpwMRKTZg2ZnFqDZTSe53k';

axios.get('http://restify-app.test/api/restify/profile', {
    headers: {
        'Authorization': `Bearer ${token}`,
        'Accept': 'application/json'
    }
})
.then(response => {
    console.log(response.data);
})
.catch(error => {
    console.error(error);
});

Here's a cURL example for retrieving the user's profile with the generated token:

curl -X GET "http://restify-app.test/api/restify/profile" \
     -H "Accept: application/json" \
     -H "Authorization: Bearer 1|f7D1qkALtM9GKDkjREKpwMRKTZg2ZnFqDZTSe53k"

Replace http://restify-app.test with your actual domain and use the authentication token you received after logging in.

Token Management

Laravel Restify uses Sanctum tokens for API authentication with the following characteristics:

Token Expiration

By default, tokens never expire. You can configure token expiration in your config/restify.php file:

PHP config/restify.php
'auth' => [
    'token_ttl' => null, // Default: tokens never expire
    // Set to minutes for token expiration
    // 'token_ttl' => 60, // Tokens expire after 60 minutes (1 hour)
]

Logout

To revoke an authentication token before it expires, use the logout endpoint:

curl -X POST "http://restify-app.test/api/logout" \
     -H "Accept: application/json" \
     -H "Authorization: Bearer 1|f7D1qkALtM9GKDkjREKpwMRKTZg2ZnFqDZTSe53k"

Successful logout returns:

{
    "message": "Successfully logged out"
}

Multiple Devices

Laravel Sanctum automatically handles multiple device authentication:

  • Each login creates a new token
  • Users can be logged in on multiple devices simultaneously
  • Each device has its own token
  • Logout only revokes the specific token used

Register

Let's see how to register a new user in the application. You can test the registration using cURL or Postman.

Use the following endpoint for registration:

http://restify-app.test/api/register

And send this payload:

{
    "name": "John Doe",
    "email": "[email protected]",
    "password": "secret!",
    "password_confirmation": "secret!"
}

Note: Email and password fields are required. The password must be confirmed (a matching password_confirmation) and at least 6 characters. The email uniqueness check runs against the table configured in restify.auth.table (config/restify.php), which defaults to users. The email is lowercased before the uniqueness check and before it is stored, so [email protected] and [email protected] are treated as the same account. Login, forgot password and reset password look the user up by the submitted email exactly first, then lowercased. Existing rows are left unchanged and keep matching their exact casing, so no migration is required.

Now, you can send a POST request with cURL:

curl -X POST "http://restify-app.test/api/register" \
     -H "Accept: application/json" \
     -H "Content-Type: application/json" \
     -d '{
             "name": "John Doe",
             "email": "[email protected]",
             "password": "secret!",
             "password_confirmation": "secret!"
         }'

You should see the response like this:

{
    "id": "12",
    "type": "users",
    "attributes": {
        "name": "John Doe",
        "email": "[email protected]"
    },
    "meta": {
        "authorizedToShow": true,
        "authorizedToStore": false,
        "authorizedToUpdate": false,
        "authorizedToDelete": false,
        "token": "2|z8D2rkBLtN8GKDkjREKpwMRKTZg2ZnFqDZTSe53k"
    }
}

Email Verification

Email verification is only available when your User model implements the MustVerifyEmail interface.

Enable Email Verification

Update your User model to implement email verification:

PHP app/Models/User.php
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable implements MustVerifyEmail
{
    use HasApiTokens;
    
    // ... rest of your User model
}

When a user registers, their email_verified_at field is set to NULL, and Restify automatically sends a verification email using the VerifyEmail notification.

How It Works

When the MustVerifyEmail interface is implemented, during registration Restify will:

  1. Send verification email using Binaryk\LaravelRestify\Notifications\VerifyEmail
  2. Generate signed verification URL pointing directly to the Restify API endpoint
  3. Include parameters in the signed URL:
    • id → User's ID
    • hash → SHA1 hash of the user's email address

The email contains a CTA (Call-To-Action) button that links directly to the API verification endpoint.

Verification Flow

  1. Registration: User registers → email_verified_at is NULL
  2. Email Sent: User receives verification email with CTA link
  3. Direct API Call: Email CTA redirects directly to /api/restify/verify/{id}/{hash}
  4. Email Verification: API validates the signed URL and sets email_verified_at to current timestamp
  5. Frontend Redirect:
    • Success: If validation succeeds, API redirects user to frontend URL from config('restify.auth.user_verify_url')
    • Failure: If hash is invalid, API redirects to frontend with ?success=false&message=Invalid hash

Frontend Integration

With the updated flow, users click the CTA in the email and are redirected directly to the API for verification. After verification (successful or failed), they land on your frontend application.

Your frontend should handle the verification result by checking URL parameters:

Success case: User lands on your configured URL (no additional parameters)

https://your-frontend-app.com/email/verify?id=1&hash=abc123

Failure case: User lands on your configured URL with error parameters

https://your-frontend-app.com/email/verify?id=1&hash=abc123&success=false&message=Invalid hash

Frontend JavaScript example:

// Check for verification result
const urlParams = new URLSearchParams(window.location.search);
const success = urlParams.get('success');
const message = urlParams.get('message');

if (success === 'false') {
    // Handle verification failure
    console.error('Verification failed:', message);
    // Show error message to user
    showErrorMessage(message || 'Email verification failed');
} else {
    // Handle verification success (success param is absent for successful verifications)
    console.log('Email verified successfully');
    // Show success message and redirect or update UI
    showSuccessMessage('Your email has been verified successfully!');
}

cURL Example

For testing purposes, you can directly call the verification endpoint:

curl -X GET "http://restify-app.test/api/restify/verify/1/abc123hash?signature=xyz&expires=123456" \
     -H "Accept: application/json"

Note: The actual verification URL includes a signed signature and expires parameter for security.

Success Response

On successful verification, the API redirects to your configured frontend URL. For direct API calls (like cURL), you might receive a success response or redirect.

Configuration

Configure your frontend URL in config/restify.php:

'auth' => [
    'user_verify_url' => env('FRONTEND_APP_URL').'/email/verify?id={id}&hash={emailHash}',
]

AI Agent Authentication

Laravel Restify's MCP server uses the same Sanctum authentication system. AI agents authenticate using the same tokens that humans use.

Token Generation for AI Agents

Users can generate API tokens for AI agents through your application interface, or programmatically:

// Generate a token for an AI agent
$user = auth()->user();
$token = $user->createToken('AI Agent Token')->plainTextToken;

// Token can be used by AI agents for MCP server access

MCP Server Authentication

When configuring the MCP server, tokens are passed in the Authorization header:

PHP config/ai.php
Mcp::web('restify', RestifyServer::class)
    ->middleware(['auth:sanctum'])  // Same authentication as REST API
    ->name('mcp.restify');

AI Agent Usage

AI agents use the same Bearer token authentication:

# AI agent making MCP request
curl -X GET "http://restify-app.test/mcp/restify" \
     -H "Accept: application/json" \
     -H "Authorization: Bearer 1|f7D1qkALtM9GKDkjREKpwMRKTZg2ZnFqDZTSe53k"

Benefits

  • Unified Authentication: Same auth system for humans and AI agents
  • Consistent Permissions: Same authorization policies apply
  • Token Management: Same logout/expiration rules
  • Security: Same security model across all interfaces

Learn More: Check the MCP Server Guide for detailed AI agent configuration.

Forgot Password

To initiate the password reset process, use the following endpoint:

{{host}}/api/forgotPassword

And send this payload:

{
    "email": "[email protected]"
}

After making a POST request to this endpoint, an email will be sent to the provided email address containing a link to reset the password. The link looks like this:

'password_reset_url' => env('FRONTEND_APP_URL').'/password/reset?token={token}&email={email}',

This configuration can be found in the config/restify.php file. The FRONTEND_APP_URL should be set to the URL of your frontend app, where the user lands when they click the action button in the email. The "token" is a variable that will be used to reset the password later on.

You can override this template per request by sending a url field alongside the email:

{
    "email": "[email protected]",
    "url": "https://app.example.com/password/reset?token={token}&email={email}"
}

For security, url is only accepted when its scheme, host, and port all match one of: the configured password_reset_url, or config('restify.auth.frontend_app_url'). frontend_app_url defaults to env('APP_URL'), so it still covers your app URL when FRONTEND_APP_URL is unset; config('app.url') itself is never checked, since once FRONTEND_APP_URL is set to a different host, app.url is your API host and a reset link should never target it. A subdomain of an allowed host does not count as a match, an http url is rejected when the matching config entry is https, a different port is rejected even on an otherwise matching host, and a url carrying userinfo (user:pass@host) is always rejected outright. Anything that doesn't match is rejected with a 422 validation error. This stops the endpoint from being used to mail a victim a valid reset link pointing at an attacker-controlled domain.

The path, query string, and fragment are also checked, but only for their characters, not their destination: they may only contain RFC 3986 unreserved/reserved characters, narrowed to exclude [, ], (, ), <, >, ", ', `, and {/} (outside the literal {token}/{email} placeholders), plus whitespace and control characters. This is because the mailed reset link is rendered through a Markdown template, and those characters could otherwise be used to break out of the intended [text](url) link and inject a second, attacker-controlled one carrying the real token - so whatever frontend host you allow here must still not itself have an open redirect, or an attacker could send a victim through it to an arbitrary destination.

To view the email content during development, you can change the following configuration in your .env file:

MAIL_MAILER=log

This will log the email content to the laravel.log file, allowing you to see the password reset email without actually sending it.

Now, you can send a POST request with cURL:

curl -X POST "http://restify-app.test/api/forgotPassword" \
     -H "Accept: application/json" \
     -H "Content-Type: application/json" \
     -d '{
            "email": "[email protected]"
         }'

If the email is successfully sent, you'll receive a response similar to the following:

{
    "message": "Reset password link sent to your email."
}

Now, the user can follow the link in the email to reset their password.

Reset Password

After the user has received the password reset email from the Forgot Password process, they can reset their password using the following endpoint:

http://restify-app.test/api/resetPassword

The payload should include the token and email received from the password reset email:

{
    "token": "7e474bb9118e736306de27126343644a7cb0ecdaec558fdef30946d15225bc07",
    "email": "[email protected]",
    "password": "new_password",
    "password_confirmation": "new_password"
}

Now, you can send a POST request with cURL:

curl -X POST "http://restify-app.test/api/resetPassword" \
     -H "Accept: application/json" \
     -H "Content-Type: application/json" \
     -d '{
             "token": "0d20b6cfa48f2bbbb83bf913d5e329207149f74d7b22d59a383d321c7af7fd5e",
             "email": "[email protected]",
             "password": "new_password",
             "password_confirmation": "new_password"
         }'

If the password reset is successful, you should receive a response similar to the following:

{
    "message": "Your password has been successfully reset."
}

Now the user's password has been successfully reset, and they can log in with their new password.

Authentication Configuration

Laravel Restify provides several configuration options in config/restify.php under the auth key:

Available Options

PHP config/restify.php
'auth' => [
    // User model for authentication
    'user_model' => \App\Models\User::class,
    
    // Token expiration time in minutes (default: null - never expire)
    'token_ttl' => null,
    
    // Password reset URL (for frontend)
    'password_reset_url' => env('FRONTEND_APP_URL').'/password/reset?token={token}&email={email}',
    
    // Email verification URL (for frontend)
    'user_verify_url' => env('FRONTEND_APP_URL').'/email/verify?id={id}&hash={emailHash}',
],

Environment Variables

Add these to your .env file:

# Frontend application URL for auth redirects
FRONTEND_APP_URL=https://your-frontend-app.com

# Mail configuration for password reset and verification emails
MAIL_MAILER=smtp
MAIL_HOST=your-smtp-host
MAIL_PORT=587
MAIL_USERNAME=your-username
MAIL_PASSWORD=your-password
MAIL_ENCRYPTION=tls

Token Configuration

  • Default TTL: null (never expire)
  • Custom TTL: Set any value in minutes
  • Never Expire: Set token_ttl to null, false, 0 or an empty string
  • Invalid values throw: anything else must be a positive number of minutes that is at least one second. a negative number, a non-numeric value such as "60m", or a value that rounds to zero seconds (0.001) throws an InvalidArgumentException naming restify.auth.token_ttl when a token is issued, instead of silently issuing tokens that never expire
// Examples
'token_ttl' => null,  // Never expire (default)
'token_ttl' => 30,    // 30 minutes
'token_ttl' => 60,    // 1 hour
'token_ttl' => 1440,  // 24 hours

Rate Limiting

Each auth route is throttled by its own named rate limiter, registered in RestifyApplicationServiceProvider::boot(), instead of sharing Laravel's default throttle bucket:

RouteLimiter nameDefault limit
registerrestify.register6/minute, keyed by IP
loginrestify.login6/minute keyed by email + IP, and 30/minute keyed by IP alone
verifyEmailrestify.verify6/minute, keyed by IP
forgotPasswordrestify.forgotPassword6/minute, keyed by IP
resetPasswordrestify.resetPassword6/minute, keyed by IP

forgotPassword and resetPassword are deliberately keyed by IP alone, not by email: a known and an unknown email share the same bucket and get an identical 429, so the rate limit itself can't be used to tell which emails have an account.

login enforces two limits at once: 6/minute per email+IP, so a client that exhausts its limit for one email is not throttled when it retries with a different email; and 30/minute per IP regardless of email, so one IP still can't spray unlimited distinct accounts. register and verifyEmail are keyed by IP alone.

This replaces the previous single shared throttle:6,1 bucket (keyed by domain|ip), which throttled every auth route together, per IP.

You can override any of these from your own service provider by calling RateLimiter::for() yourself - your call always wins, whether it runs before or after Restify's, since RateLimiter::for() simply replaces whatever is registered under that name and Restify only fills in a name that is still unset:

use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;

RateLimiter::for('restify.login', function ($request) {
    return Limit::perMinute(10)->by($request->ip());
});

Customizing Authentication Controllers

You can publish the authentication controllers from the Restify package to your own application, allowing you to customize their behavior as needed. To publish the controllers, run the following command:

php artisan restify:auth

This command will copy the authentication controllers to the app/Http/Controllers/Restify directory in your Laravel project.

The command accepts an optional --actions parameter, which allows you to specify which controllers you want to publish. If no action is passed, the command will publish all controllers and the ForgotPasswordNotification. For example, to publish only the login and register controllers, run:

php artisan restify:auth --actions=login,register

Now, you can make any necessary changes to these controllers to fit your specific requirements.

Customizing the Register Route

In a real-world scenario, you might need to customize only the register route. To do this, you can use the restify:auth command with the --actions option to publish only the register controller:

php artisan restify:auth --actions=register

After running the command, the register controller will be published to your application, and you can modify it to fit your requirements.

For example, if you previously published the register route, your routes/api.php file might look like this:

// ...

Route::restifyAuth(actions: ['login', 'logout', 'verifyEmail', 'forgotPassword', 'resetPassword']);

// ...

You can run the restify:auth command again with a different --actions value at any time - there's no need to edit routes/api.php by hand first. The command merges the newly published actions into the existing list for you, and re-running it for an action that's already published is a safe no-op.

Laravel Restify is built by BinarCode - also makers of Routines, the AI assistant for your Mac.