ssbhattarai/magiclink

A passwordless login system for laravel

v1.0.1 2025-08-11 15:39 UTC

This package is auto-updated.

Last update: 2025-08-11 15:39:42 UTC


README

Latest Version on Packagist Total Downloads License Tests

A passwordless login system for Laravel that allows users to log in using magic links sent to their email.

Zero passwords, maximum security! Send secure, time-limited login links directly to users' inboxes.

Features

  • ๐Ÿ” Passwordless Authentication - Secure login without passwords
  • โฐ Time-Limited Links - Configurable expiration times for security
  • ๐Ÿ“ง Beautiful Email Templates - Professional, responsive email design
  • ๐ŸŽจ Customizable Views - Easily customize email and request form templates
  • ๐Ÿ”„ Queue Support - Send emails asynchronously for better performance
  • ๐Ÿงน Auto Cleanup - Automatic expired token cleanup
  • ๐Ÿ› ๏ธ Artisan Commands - Built-in commands for diagnostics and maintenance
  • ๐Ÿ” Comprehensive Testing - 100% test coverage for reliability
  • ๐Ÿ“ฑ Mobile-Friendly - Responsive design for all devices

๐Ÿ“‹ Requirements

Laravel Version PHP Version Package Version
10.x - 11.x >= 8.2 ^1.0
9.x >= 8.1 Contact author

๐Ÿ“ฆ Installation

1. Install via Composer

composer require ssbhattarai/magiclink

2. Publish Configuration

php artisan vendor:publish --provider="Ssbhattarai\MagicLink\MagicLinkServiceProvider" --tag="magiclink-config"

3. Run Migrations

php artisan migrate

4. Optional: Publish Views (for customization)

php artisan vendor:publish --provider="Ssbhattarai\MagicLink\MagicLinkServiceProvider" --tag="magiclink-views"

5. Optional: Publish Migrations (for customization)

php artisan vendor:publish --provider="Ssbhattarai\MagicLink\MagicLinkServiceProvider" --tag="magiclink-migrations"

Configuration

The configuration file config/magiclink.php provides several customization options:

<?php

return [
    // Link expiration time in minutes
    'link_expiration' => 15,
    
    // Redirect URL after successful login
    'login_redirect' => '/dashboard',
    
    // Email subject line
    'email_subject' => 'Your Magic Login Link',
    
    // Route prefix for magic link endpoints
    'prefix' => 'magic-link',
    
    // Middleware applied to magic link routes
    'middleware' => ['web'],
    
    /*
    |--------------------------------------------------------------------------
    | Rate Limiting
    |--------------------------------------------------------------------------
    |
    | Configure rate limiting for magic link requests to prevent abuse.
    | You can set different limits for different scenarios.
    |
    */
    'rate_limiting' => [
        // Rate limit for magic link requests per email address
        'per_email' => [
            'max_attempts' => 3,        // Maximum attempts
            'decay_minutes' => 60,      // Time window in minutes
        ],
        
        // Rate limit for magic link requests per IP address
        'per_ip' => [
            'max_attempts' => 10,       // Maximum attempts
            'decay_minutes' => 60,      // Time window in minutes
        ],
        
        // Global rate limit for all magic link requests
        'global' => [
            'max_attempts' => 100,      // Maximum attempts
            'decay_minutes' => 60,      // Time window in minutes
        ],
        
        // Enable/disable rate limiting
        'enabled' => true,
    ],
];

Configuration Options

Option Type Default Description
link_expiration integer 15 Link validity in minutes
login_redirect string '/dashboard' Post-login redirect URL
email_subject string 'Your Magic Login Link' Email subject line
prefix string 'magic-link' Route prefix
middleware array ['web'] Applied middleware
rate_limiting.enabled boolean true Enable/disable rate limiting
rate_limiting.per_email.max_attempts integer 3 Max attempts per email
rate_limiting.per_email.decay_minutes integer 60 Rate limit window for email
rate_limiting.per_ip.max_attempts integer 10 Max attempts per IP
rate_limiting.per_ip.decay_minutes integer 60 Rate limit window for IP
rate_limiting.global.max_attempts integer 100 Global max attempts
rate_limiting.global.decay_minutes integer 60 Global rate limit window

Usage

Basic Usage

1. Request a Magic Link

Users can request a magic link by visiting the magic link form:

GET /magic-link

Or programmatically:

use Ssbhattarai\MagicLink\Services\MagicLinkService;

$magicLinkService = app(MagicLinkService::class);

try {
    $magicLinkService->sendMagicLink('user@example.com');
    // Success: Magic link sent
} catch (\Exception $e) {
    // Handle error: User not found, rate limited, or email failed
    if (str_contains($e->getMessage(), 'Too many')) {
        // Rate limit exceeded
        return back()->withErrors(['email' => 'Rate limit exceeded. Please try again later.']);
    }
    return back()->withErrors(['email' => $e->getMessage()]);
}

Rate Limiting Usage

Check remaining attempts for an email:

use Ssbhattarai\MagicLink\Services\MagicLinkService;

$service = app(MagicLinkService::class);
$remainingAttempts = $service->getRemainingAttempts('user@example.com');

if ($remainingAttempts <= 0) {
    return response()->json(['error' => 'Rate limit exceeded'], 429);
}

Clear rate limits programmatically:

use Ssbhattarai\MagicLink\Services\MagicLinkService;

$service = app(MagicLinkService::class);
$service->clearRateLimit('user@example.com', $request);

2. Login via Magic Link

When users click the magic link from their email, they're automatically logged in:

GET /magic-link/login/{token}

Available Routes

Method URI Name Description
GET /magic-link magiclink.request Show magic link request form
POST /magic-link/request magiclink.request Process magic link request
GET /magic-link/login/{token} magiclink.login Login with magic link token

Customization

Custom Email Template

After publishing views, customize the email template at:

resources/views/vendor/magiclink/email.blade.php

Example Custom Template:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Welcome Back!</title>
</head>
<body style="font-family: Arial, sans-serif;">
    <div style="max-width: 600px; margin: 0 auto; padding: 20px;">
        <h1>Welcome Back! ๐Ÿ‘‹</h1>
        <p>Click the button below to access your account:</p>
        
        <a href="{{ $link }}" 
           style="background: #4F46E5; color: white; padding: 12px 24px; 
                  text-decoration: none; border-radius: 6px; display: inline-block;">
            Access My Account
        </a>
        
        <p><small>This link expires in {{ config('magiclink.link_expiration') }} minutes.</small></p>
    </div>
</body>
</html>

Custom Request Form

Customize the magic link request form at:

resources/views/vendor/magiclink/magic-link.blade.php

Using Your Own Controller

Create a custom controller to handle magic link requests:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Ssbhattarai\MagicLink\Services\MagicLinkService;

class AuthController extends Controller
{
    public function requestMagicLink(Request $request, MagicLinkService $service)
    {
        $request->validate(['email' => 'required|email|exists:users,email']);
        
        try {
            $service->sendMagicLink($request->email);
            return response()->json(['message' => 'Magic link sent successfully!']);
        } catch (\Exception $e) {
            return response()->json(['error' => $e->getMessage()], 400);
        }
    }
}

Database Schema

The package creates a magic_links table with the following structure:

Schema::create('magic_links', function (Blueprint $table) {
    $table->id();
    $table->string('email')->index();
    $table->string('token')->unique();
    $table->timestamp('expires_at');
    $table->timestamp('used_at')->nullable();
    $table->timestamps();
    
    $table->index(['email', 'expires_at'], 'idx_email_expires');
    $table->index('token', 'idx_token');
});

Fields Description

Field Type Description
id bigint Primary key
email string User's email address
token string Unique magic link token
expires_at timestamp Token expiration time
used_at timestamp When token was used (nullable)
created_at timestamp Token creation time
updated_at timestamp Last update time

Artisan Commands

Check Installation

Verify that the package is properly installed and configured:

php artisan magiclink:check

Output Example:

Checking MagicLink package installation...
โœ… Routes are registered:
   POST magic-link/request (magiclink.request)
   GET magic-link/login/{token} (magiclink.login)
โœ… Configuration loaded
โœ… User model found: App\Models\User
โœ… Database table "magic_links" exists
   Total tokens: 15
   Expired tokens: 3
โš ๏ธ Using sync queue driver (emails sent immediately)

Cleanup Expired Tokens

Remove expired tokens from the database:

php artisan magiclink:cleanup

Clear Rate Limits

Clear rate limits for magic link requests:

# Clear rate limit for specific email
php artisan magiclink:clear-rate-limit user@example.com

# Clear all rate limits
php artisan magiclink:clear-rate-limit

Rate Limiting Status

Check the current rate limiting status:

php artisan magiclink:rate-limit

Output Example:

Checking MagicLink rate limiting status...
โœ… Rate limiting is enabled
โœ… Global rate limit: 100 attempts, 60 minutes
โœ… Email rate limit: 3 attempts, 60 minutes
โœ… IP rate limit: 10 attempts, 60 minutes

Troubleshooting

404 Errors

If you're getting 404 errors after installation:

  1. Clear caches:

    php artisan route:clear
    php artisan config:clear
    php artisan cache:clear
  2. Verify routes:

    php artisan route:list | grep magic-link
  3. Check User model: Ensure your User model exists at App\Models\User

Email Not Sending

  1. Check queue configuration:

    php artisan queue:work
  2. Verify mail configuration in .env:

     MAIL_MAILER=smtp
     MAIL_SCHEME=null
     MAIL_HOST=sandbox.smtp.mailtrap.io // you can add yours
     MAIL_PORT=2525
     MAIL_USERNAME=usermae
     MAIL_PASSWORD=password
     MAIL_FROM_ADDRESS="no-reply@example.com"
     MAIL_FROM_NAME="${APP_NAME}"
  3. Test email configuration:

    php artisan tinker
    Mail::raw('Test', function($m) { $m->to('test@example.com'); });

Token Validation Issues

  1. Check system time: Ensure server time is correct for expiration validation

  2. Verify token in database:

    SELECT * FROM magic_links WHERE token = 'your_token';

Testing

Run Package Tests

composer test

Individual Test Commands

# Linting
composer test:lint

# Type coverage
composer test:type-coverage

# Unit tests with coverage
composer test:unit

# Static analysis
composer test:types

Example Test Case

<?php

use Ssbhattarai\MagicLink\Services\MagicLinkService;
use Tests\TestCase;

class MagicLinkTest extends TestCase
{
    public function test_can_send_magic_link()
    {
        $user = User::factory()->create(['email' => 'test@example.com']);
        
        $service = app(MagicLinkService::class);
        $service->sendMagicLink('test@example.com');
        
        $this->assertDatabaseHas('magic_links', [
            'email' => 'test@example.com'
        ]);
    }
    
    public function test_can_login_with_valid_token()
    {
        $user = User::factory()->create(['email' => 'test@example.com']);
        
        $service = app(MagicLinkService::class);
        $service->sendMagicLink('test@example.com');
        
        $token = MagicLink::where('email', 'test@example.com')->first()->token;
        
        $response = $this->get(route('magiclink.login', $token));
        
        $response->assertRedirect(config('magiclink.login_redirect'));
        $this->assertAuthenticatedAs($user);
    }
}

Security Considerations

Best Practices

  1. Use HTTPS: Always use HTTPS in production to protect magic links in transit
  2. Short Expiration: Keep link expiration times short (10/15 minutes recommended)
  3. Single Use: Tokens are automatically invalidated after use
  4. Email Validation: Verify email addresses belong to real users
  5. Rate Limiting: Implement rate limiting on magic link requests

Implementation Example

// In your RouteServiceProvider or controller
Route::middleware(['throttle:5,1'])->group(function () {
    Route::post('/magic-link/request', [MagicLinkController::class, 'requestLink']);
});

Contributing

We welcome contributions! Please follow these steps:

  1. Fork the repository
  2. Create a feature branch: git checkout -b feature/amazing-feature
  3. Run tests: composer test
  4. Commit changes: git commit -m 'Add amazing feature'
  5. Push to branch: git push origin feature/amazing-feature
  6. Open a Pull Request

Development Setup

git clone https://github.com/ssbhattarai/magiclink.git
cd magiclink
composer install
composer test

API Reference

MagicLinkService

sendMagicLink(string $email): void

Sends a magic link to the specified email address.

Parameters:

  • $email (string): User's email address

Throws:

  • \Exception: If user not found

Example:

$service = app(MagicLinkService::class);
$service->sendMagicLink('user@example.com');

loginWithToken(string $token): User

Authenticates user with magic link token.

Parameters:

  • $token (string): Magic link token

Returns:

  • User: Authenticated user instance

Throws:

  • \Exception: If token invalid or expired

cleanupExpiredTokens(): int

Removes expired tokens from database.

Returns:

  • int: Number of deleted tokens

MagicLink Model

Scopes

// Get valid tokens
MagicLink::valid()->get();

// Get expired tokens
MagicLink::expired()->get();

// Get used tokens
MagicLink::used()->get();

Methods

$token = MagicLink::findValidToken('token_string');
$token->markAsUsed();
$token->isValid(); // bool
$token->isExpired(); // bool
$token->isUsed(); // bool

Changelog

v1.0.0 (Latest)

  • โœจ Initial release
  • ๐Ÿ” Passwordless authentication system
  • ๐Ÿ“ง Beautiful email templates
  • ๐Ÿ› ๏ธ Artisan commands for management
  • ๐Ÿงช 100% test coverage

See CHANGELOG.md for detailed version history.

License

This package is open-sourced software licensed under the MIT license.

Author

Shyam Sundar Bhattarai

Acknowledgments

  • Laravel team for the amazing framework
  • All contributors who help improve this package
  • The open-source community for inspiration and feedback

Support

If you discover any security vulnerabilities, please send an e-mail to nascentsurya@gmail.com.

For general questions and support:

Made with โค๏ธ for the Laravel community

โญ Star this repo if it helped you!