Tyro Login

repository·main·Indexed 42 days ago

https://github.com/hasinhayder/tyro-login

A production-ready authentication UI kit for Laravel 12 and 13. It provides professional login and registration pages with 10 premium layouts and advanced security features including Passkeys (WebAuthn), TOTP 2FA, Email OTP, Social Login via Laravel Socialite, and lockout protection.

Tokens
55.5K
Snippets
165
Records
197
Agent score
53%

What's inside Tyro Login

  1. Overview of Tyro Login

    main

    Tyro Login is a Laravel authentication infrastructure package designed to provide guards, providers, sessions, tokens, verification, and security for the Tyro ecosystem. It is intended as authentication infrastructure rather than a simple login page generator or starter kit.

    Technical Specifications:

    • Composer Package: hasinhayder/tyro-login
    • Namespace: HasinHayder\\TyroLogin
    • PHP Version: ^8.2
    • Laravel Version: ^12.0 || ^13.0
  2. Manage backward compatibility with versioned config

    main

    When introducing changes to default behaviors in new versions, use versioned configuration keys. This allows existing users to opt-in to new behavior while maintaining their current setup after an upgrade.

    Example: Instead of changing a hardcoded redirect, use a config key like tyro-login.redirects.after_login which defaults to an existing behavior (e.g., 'intended').

  3. Use Cache for Transient Tokens and Lockouts

    main

    Use the Cache (not the database) for transient, self-cleaning data such as:

    • OTP codes
    • Password reset tokens
    • Email verification tokens
    • Magic link hashes
    • Lockout counters

    Implementation Rules:

    • Prefix all cache keys with tyro-login. to prevent collisions.
    • Set the TTL (Time To Live) using values from your configuration.
    • Explicitly clear cache keys upon successful completion (e.g., after a password change).
    // Cache with TTL - self-cleaning
    Cache::put(
        'tyro-login.reset:' . $token,
        ['email' => $email],
        now()->addMinutes(config('tyro-login.password_reset.expire', 60))
    );
  4. Implement standalone Mailables for each email type

    main

    To ensure testability and extensibility, Tyro Login follows a single-responsibility pattern where each email type has its own dedicated Mailable class. Avoid creating a single generic mailable that uses a $type parameter to switch behavior.

    Available Mailables in the package:

    • OtpMail
    • PasswordResetMail
    • VerifyEmailMail
    • WelcomeMail
    • MagicLinkMail
    // Correct: Single-purpose mailable
    class OtpMail extends Mailable
    {
        use Queueable, SerializesModels;
    
        public function __construct(
            public string $otp,
            public string $userName,
            public int $expiresInMinutes,
        ) {}
    
        public function build(): self
        {
            return $this->subject(
                config('tyro-login.emails.otp.subject', 'Your One-Time Password')
            )->view('tyro-login::emails.otp');
        }
    }
  5. Implement dual validation for password resets

    main

    To ensure security and prevent URL tampering, use a dual validation strategy:

    1. Cache-stored token: Stores user data and ensures the token is single-use and expires.
    2. Laravel Signed URL: Ensures the reset link has not been modified.

    Both the cache entry and the signed URL should share the same expiration time. The cache key pattern is tyro-login:password-reset:{token}.

    // Cache token + signed URL
    $expiresInMinutes = (int) config('tyro-login.password_reset.expire', 60);
    $expiresAt = now()->addMinutes($expiresInMinutes);
    
    Cache::put("tyro-login:password-reset:{$token}", [
        'user_id' => $user->id,
        'email' => $user->email,
    ], $expiresAt);
    
    $url = URL::temporarySignedRoute(
        'tyro-login.password.reset',
        $expiresAt,
        ['token' => $token]
    );
  6. Understand the TyroLoginServiceProvider lifecycle

    main

    The TyroLoginServiceProvider is the bootstrap entrypoint for the package. It follows strict Laravel service provider lifecycle rules to ensure stability:

    1. register() phase: This method is used exclusively for merging configuration. Do not perform boot logic (like loading routes or views) here, as application services may not be available yet.
    2. boot() phase: This is where all functional logic resides. The package follows a strict internal execution order to prevent dependency issues:
      • Routes (must be first so they can be referenced)
      • Views
      • Migrations
      • Publishing
      • Commands
      • Auth Redirection (must be last so middleware can access all routes)

    Use mergeConfigFrom() in register() to allow your application's configuration to override package defaults.

    // Correct implementation pattern
    public function register(): void
    {
        $this->mergeConfigFrom(__DIR__ . '/../../config/tyro-login.php', 'tyro-login');
    }
    
    public function boot(): void
    {
        $this->registerRoutes();
        $this->registerViews();
        $this->registerMigrations();
        $this->registerPublishing();
        $this->registerCommands();
        $this->configureAuthRedirection();
    }
  7. Understand the Public API Contract

    main

    The public API of Tyro Login is a strict contract. Any change to the following elements is considered a breaking change and requires a major version bump:

    • Configuration keys (e.g., tyro-login.features.remember_me)
    • Route names (e.g., tyro-login.login)
    • Method signatures (both public and protected methods in controllers)
    • Named parameters
    • Blade sections or stacks
    • Published view structures
    • Static methods
    • Command signatures
    • Event classes
    • Mailables

    To avoid breaking changes when renaming routes, use the configuration to provide overrides. For example, if a route name changes, allow users to map the old name to the new one via the routes config key.

  8. Use POST for state mutations and GET for form display

    main

    Follow HTTP semantics to prevent CSRF attacks and accidental state changes (like browser prefetching logging a user out):

    • GET: Use exclusively for displaying forms (e.g., login form, register form). These routes should return View responses and have no side effects.
    • POST: Use for all state mutations (e.g., login submission, register submission, logout, OTP verification, 2FA confirmation, password reset tokens). These routes should return RedirectResponse responses.

    For sensitive actions like logout, it is recommended to allow GET only to show a confirmation page or redirect away, while the actual mutation is handled via POST.

    // Correct pattern for a login flow
    Route::get('login', [LoginController::class, 'showLoginForm'])->name('tyro-login.login');
    Route::post('login', [LoginController::class, 'login']);
    
    // Correct pattern for logout to prevent accidental GET-based logouts
    Route::match(['get', 'post'], 'logout', [LoginController::class, 'logout'])->name('tyro-login.logout')->middleware('auth');
    
    // Controller logic example
    public function logout(Request $request): RedirectResponse
    {
        if ($request->isMethod('post')) {
            // ... process logout
        }
        return redirect()->route('tyro-login.login');
    }
  9. Prevent self-referrals and duplicate referrals

    main

    To maintain the integrity of the referral system, the following rules must be applied during the referral tracking process:

    Self-Referral Prevention

    Compare the user_id of the InvitationLink (the referrer) with the $newUserId (the registrant). If they are identical, the attempt is a self-referral.

    • Action: Log a warning with the [Tyro-Login] Self-referral attempt detected message and return null silently so registration is not interrupted.

    Duplicate Referral Prevention

    A user can only be referred once. Before creating a new InvitationReferral record, check if a record already exists for the referred_user_id.

    • Action: If an existing record is found, log the event for auditing and return the existing referral instead of creating a duplicate.
  10. Implement dual verification with Cache and Signed URLs

    main

    Email verification should use two layers of validation:

    1. A cache-stored token: Ensures the token is valid, unexpired, and single-use. Use the cache key pattern tyro-login:email-verify:{token}.
    2. A Laravel signed URL: Ensures the URL parameters have not been tampered with.

    Both the cache entry and the signed URL should share the same expiration time, which is configurable via tyro-login.verification.expire (defaults to 60 minutes).

    $expiresInMinutes = (int) config('tyro-login.verification.expire', 60);
    $expiresAt = now()->addMinutes($expiresInMinutes);
    
    Cache::put("tyro-login:email-verify:{$token}", [
        'user_id' => $user->id,
        'email' => $user->email,
    ], $expiresAt);
    
    $url = URL::temporarySignedRoute(
        'tyro-login.verification.verify',
        $expiresAt,
        ['token' => $token]
    );
  11. Maintain thin Eloquent models

    main

    To avoid coupling the package with your application's business logic, keep models 'thin'. Models should primarily define structural properties like $table, $fillable, $hidden, $casts, $timestamps, and relationships.

    Guidelines:

    • Allowed: Simple static lookup methods (e.g., findByProvider()).
    • Avoid: Complex business logic or methods that perform side effects (e.g., linkUser()).
    • Where to put logic: Move complex operations to controllers, helpers, or dedicated service classes.
    // Correct: Thin model focusing on structure
    class SocialAccount extends Model
    {
        protected $fillable = [
            'user_id', 'provider', 'provider_user_id', 'provider_email',
            'provider_avatar', 'access_token', 'refresh_token', 'token_expires_at',
        ];
    
        protected $hidden = [
            'access_token', 'refresh_token',
        ];
    
        protected $casts = [
            'access_token' => EncryptedOrPlaintext::class,
            'refresh_token' => EncryptedOrPlaintext::class,
            'token_expires_at' => 'datetime',
        ];
    
        public function user(): BelongsTo
        {
            return $this->belongsTo(config('tyro-login.user_model'));
        }
    }
  12. Access Configuration via the `config()` Helper

    main

    The config/tyro-login.php file is the single source of truth for all package settings.

    Never call env() directly inside controllers, commands, mailables, or views. Doing so bypasses the configuration file's defaults, casting, and documentation, and causes the application to ignore any changes a user makes to their published config/tyro-login.php file.

    Always use the config() helper to retrieve values.

    // Correct: Controller reads from config
    public function showLoginForm()
    {
        $layout = config('tyro-login.layout', 'centered');
    }
    
    // Incorrect: Controller reads env directly (bypasses config)
    public function showLoginForm()
    {
        $layout = env('TYRO_LOGIN_LAYOUT', 'centered');
    }