16 KiB
Profile Direct Login Feature
Overview
This feature allows you to create direct links to User, Organization, Bank, and Admin profile logins that can be used in emails or other external communications. The system handles the authentication flow automatically for all profile types, with automatic username pre-filling for better user experience.
How It Works
The direct login route implements a secure, multi-step authentication flow:
-
User Authentication Check
- If the user is not logged in with their personal User profile, they are redirected to the user login page
- For User profiles: Username is automatically pre-filled on the login form via URL parameter
- After successful user login, they are automatically redirected back to continue the profile switch flow
-
Relationship Verification
- The system verifies that the authenticated user has access to the specified profile:
- User: Authenticated user must match the target user
- Organization: User owns/is a member of the organization
- Bank: User manages the bank
- Admin: User has this admin profile
- If the user doesn't have access, a 403 Forbidden error is returned
- The system verifies that the authenticated user has access to the specified profile:
-
Profile Switch
- User: Direct redirect to main page (or custom intended URL)
- Organization: Direct switch without password (matches normal profile switching behavior)
- Bank: Redirected to bank password entry page
- Admin: Redirected to admin password entry page
-
Final Redirect
- User: Redirected to intended URL or main page
- Organization/Bank/Admin: Redirected to intended URL or main page after profile switch
Usage
User Profile
To create a link to a user login:
route('user.direct-login', ['userId' => $user->id])
Example URL:
https://yoursite.com/user/123/login
With custom intended destination (e.g., profile edit) and username pre-fill:
route('user.direct-login', [
'userId' => $user->id,
'intended' => route('profile.edit'),
'name' => $user->name // Optional: pre-fills username on login form
])
Example URL with username pre-fill:
https://yoursite.com/nl/user/123/login?intended=https%3A%2F%2Fyoursite.com%2Fnl%2Fprofiel%2Fbewerken&name=johndoe
Note:
- User direct login defaults to redirecting to the main page after login, but you can override this with the
intendedparameter - The
nameparameter is automatically included when generating links viaProfileEditedByAdminMailfor improved user experience - When the user is redirected to the login page, the username field will be automatically pre-filled with the value from the
nameparameter
Organization Profile
To create a link to an organization login:
route('organization.direct-login', ['organizationId' => $organization->id])
Example URL:
https://yoursite.com/organization/123/login
With intended destination:
route('organization.direct-login', [
'organizationId' => $organization->id,
'intended' => route('organization.settings')
])
Bank Profile
To create a link to a bank login:
route('bank.direct-login', ['bankId' => $bank->id])
Example URL:
https://yoursite.com/bank/456/login
With intended destination:
route('bank.direct-login', [
'bankId' => $bank->id,
'intended' => route('transactions.review')
])
Admin Profile
To create a link to an admin login:
route('admin.direct-login', ['adminId' => $admin->id])
Example URL:
https://yoursite.com/admin/789/login
With intended destination:
route('admin.direct-login', [
'adminId' => $admin->id,
'intended' => route('admin.dashboard')
])
In Email Templates
<a href="{{ route('organization.direct-login', ['organizationId' => $organization->id]) }}">
Login to {{ $organization->name }}
</a>
Or with a specific destination:
<a href="{{ route('organization.direct-login', ['organizationId' => $organization->id, 'intended' => route('post.edit', $post->id)]) }}">
Edit this post as {{ $organization->name }}
</a>
Security Features
Multi-Layer Authentication
- User Guard First: Ensures base authentication is established
- Relationship Verification: Only users with proper access can switch to the organization
- Password Re-Authentication: Separate password required for organization access
Session Management
- Profile switch intent is stored in encrypted session
- Intended URLs are validated and sanitized
- Sessions are cleared after successful authentication
Access Control
- 404 error if organization doesn't exist
- 403 error if user doesn't have access to organization
- All authentication follows the existing SwitchGuardTrait security pattern
Flow Diagram
User clicks email link
|
v
Is user authenticated? --NO--> User Login --> [Back to this flow]
|
YES
v
Does user own/manage org? --NO--> 403 Forbidden
|
YES
v
Set profile switch intent
|
v
Organization password page
|
v
Password correct? --NO--> Error message
|
YES
v
Switch to Organization guard
|
v
Redirect to intended URL or main page
Implementation Details
Route Definitions
Located in routes/web.php:
User Route (Guest accessible - no auth middleware):
// Located in the guest routes section (line ~182)
// Accessible to both authenticated and non-authenticated users
Route::get('/user/{userId}/login', [UserLoginController::class, 'directLogin'])
->name('user.direct-login');
Organization/Bank/Admin Routes (Inside auth middleware group):
// Located inside the auth middleware group (line ~439+)
// Require user authentication before accessing
Route::get('/organization/{organizationId}/login', [OrganizationLoginController::class, 'directLogin'])
->name('organization.direct-login');
Route::get('/bank/{bankId}/login', [BankLoginController::class, 'directLogin'])
->name('bank.direct-login');
Route::get('/admin/{adminId}/login', [AdminLoginController::class, 'directLogin'])
->name('admin.direct-login');
Important: The user route is placed in the guest routes section (outside auth middleware) to allow unauthenticated users to access it. The controller handles authentication checks internally and redirects to login when needed.
Controller Methods
Located in:
app/Http/Controllers/UserLoginController.phpapp/Http/Controllers/OrganizationLoginController.phpapp/Http/Controllers/BankLoginController.phpapp/Http/Controllers/AdminLoginController.php
Each directLogin() method handles:
- Profile existence validation
- User authentication check
- Relationship verification:
- User: Authenticated user must match target user
- Organization: User owns/is a member of the organization
- Bank: User manages the bank
- Admin: User has this admin profile
- Session intent setting (for Organization, Bank, Admin)
- Proper redirects for each step
User-specific features:
- Username Pre-fill: When redirecting to login, the
namequery parameter is preserved and passed to the login page - Localized URLs: Uses
LaravelLocalizationto generate properly localized redirect URLs - URL Parameter Handling: The
nameparameter from the direct login URL is forwarded to the login page as a query parameter - The login form reads the
nameparameter viarequest()->input('name')and pre-fills the username field
Login Form Implementation
The login form in resources/views/auth/login.blade.php supports username pre-filling via URL parameter:
<form method="POST" action="{{ route('login') }}">
@csrf
<div>
<x-jetstream.label for="name" value="{!! __('Email or username') !!}" />
<x-jetstream.input
id="name"
class="block mt-1 w-full"
type="text"
name="name"
value="{{ old('name', request()->input('name')) }}"
required
autofocus
autocomplete="username"
/>
</div>
<!-- Password field... -->
</form>
Key points:
- The
valueattribute usesold('name', request()->input('name'))to:- First check for validation errors (old input)
- Fall back to the URL query parameter if no old input exists
- The
autocomplete="username"attribute helps browsers identify the field correctly - This works for direct URL access (e.g.,
/nl/inloggen?name=johndoe) as well as redirects
Session Keys Used
For all profile types:
url.intended: Laravel's standard intended redirect URL (used for user login redirect back to profile login)intended_profile_switch_type: Set to 'Organization', 'Bank', or 'Admin'intended_profile_switch_id: Profile ID
Profile-specific:
organization_login_intended_url: Optional final destination URL after organization loginbank_login_intended_url: Optional final destination URL after bank loginadmin_login_intended_url: Optional final destination URL after admin login
Examples
Example 1: Simple Profile Login Links
// User
$url = route('user.direct-login', ['userId' => 123]);
// Result: https://yoursite.com/user/123/login
// Organization
$url = route('organization.direct-login', ['organizationId' => 5]);
// Result: https://yoursite.com/organization/5/login
// Bank
$url = route('bank.direct-login', ['bankId' => 2]);
// Result: https://yoursite.com/bank/2/login
// Admin
$url = route('admin.direct-login', ['adminId' => 1]);
// Result: https://yoursite.com/admin/1/login
Example 2: Deep Links to Specific Pages
// User: Direct link to profile edit page
$url = route('user.direct-login', [
'userId' => $user->id,
'intended' => route('profile.edit')
]);
// Organization: Direct link to post management page
$url = route('organization.direct-login', [
'organizationId' => $org->id,
'intended' => route('posts.index')
]);
// Bank: Direct link to transaction review
$url = route('bank.direct-login', [
'bankId' => $bank->id,
'intended' => route('transactions.pending')
]);
// Admin: Direct link to user management
$url = route('admin.direct-login', [
'adminId' => $admin->id,
'intended' => route('admin.users')
]);
Example 3: In Blade Email Templates
User Profile Edited Notification (Automated via ProfileEditedByAdminMail):
The ProfileEditedByAdminMail class automatically generates properly localized URLs with username pre-fill:
// In ProfileEditedByAdminMail.php constructor:
$profileEditPath = LaravelLocalization::getURLFromRouteNameTranslated(
$this->locale,
'routes.profile.edit'
);
$profileEditUrl = url($profileEditPath);
// Direct user login with redirect to profile.edit and username pre-filled
$this->buttonUrl = LaravelLocalization::localizeURL(
route('user.direct-login', [
'userId' => $profile->id,
'intended' => $profileEditUrl,
'name' => $profile->name // Username pre-fill
]),
$this->locale
);
Generated URL example:
https://yoursite.com/nl/user/2/login?intended=https%3A%2F%2Fyoursite.com%2Fnl%2Fprofiel%2Fbewerken&name=johndoe
Email template usage:
@component('emails.layouts.html', ['locale' => $locale])
<p>Hello {{ $profile->full_name ?? $profile->name }},</p>
<p>An administrator has made changes to your profile.</p>
<table role="presentation" class="mobile-button" cellpadding="0" cellspacing="0" border="0" align="center">
<tr>
<td align="center" style="background-color: {{ theme_color('brand') }};">
<a href="{{ $buttonUrl }}" style="color: #ffffff;">
Review Your Profile
</a>
</td>
</tr>
</table>
@endcomponent
Organization Event Review:
@component('emails.layouts.html', ['locale' => 'en'])
<p>Hello {{ $user->name }},</p>
<p>A new event requires your organization's approval.</p>
<table role="presentation" class="mobile-button" cellpadding="0" cellspacing="0" border="0" align="center">
<tr>
<td align="center" style="background-color: {{ theme_color('brand') }};">
<a href="{{ route('organization.direct-login', [
'organizationId' => $organization->id,
'intended' => route('event.review', $event->id)
]) }}" style="color: #ffffff;">
Review Event as {{ $organization->name }}
</a>
</td>
</tr>
</table>
@endcomponent
Bank Transaction Alert:
@component('emails.layouts.html', ['locale' => 'en'])
<p>Hello {{ $user->name }},</p>
<p>A large transaction requires bank approval.</p>
<table role="presentation" class="mobile-button" cellpadding="0" cellspacing="0" border="0" align="center">
<tr>
<td align="center" style="background-color: {{ theme_color('brand') }};">
<a href="{{ route('bank.direct-login', [
'bankId' => $bank->id,
'intended' => route('transaction.show', $transaction->id)
]) }}" style="color: #ffffff;">
Review Transaction as {{ $bank->name }}
</a>
</td>
</tr>
</table>
@endcomponent
Admin User Report:
@component('emails.layouts.html', ['locale' => 'en'])
<p>Hello {{ $user->name }},</p>
<p>A user has been reported and requires admin review.</p>
<table role="presentation" class="mobile-button" cellpadding="0" cellspacing="0" border="0" align="center">
<tr>
<td align="center" style="background-color: {{ theme_color('brand') }};">
<a href="{{ route('admin.direct-login', [
'adminId' => $admin->id,
'intended' => route('admin.reports.show', $report->id)
]) }}" style="color: #ffffff;">
Review Report as Admin
</a>
</td>
</tr>
</table>
@endcomponent
Troubleshooting
User Gets 404 Error
- The profile ID doesn't exist
- Check that the profile hasn't been soft-deleted
- Verify correct profile type is being used
User Gets 403 Error
- User doesn't have access to the profile
- For User: Authenticated user ID doesn't match target user ID
- For Organization: Verify relationship exists in
organization_usertable - For Bank: Verify user is listed as manager in
bank_managerstable - For Admin: Verify user has this admin profile in
admin_usertable
Redirect Loop
- Ensure user login is working correctly
- Check that session storage is configured properly
- Verify middleware chain is correct
Username Not Pre-filled on Login Form
- Check that the
nameparameter is included in the direct login URL - Verify the login form is using
request()->input('name')to read the parameter - Check browser developer tools to confirm the query parameter is present in the URL
- Ensure the login view has:
value="{{ old('name', request()->input('name')) }}"
User Direct Login Redirects to Login Without Query Parameters
Problem: Query parameters (intended and name) are stripped during redirect
Cause: The user.direct-login route is inside the auth middleware group, which redirects unauthenticated users to login before the controller can handle the parameters
Solution: Ensure the user.direct-login route is defined in the guest routes section of routes/web.php (around line 182), NOT inside the auth middleware group. The route should only have localization middleware, not authentication middleware:
// ✅ Correct - in guest routes section
Route::get('/user/{userId}/login', [UserLoginController::class, 'directLogin'])
->name('user.direct-login');
// ❌ Wrong - inside auth middleware group
Route::middleware(['auth:web'])->group(function () {
Route::get('/user/{userId}/login', [...]) // Don't place here!
});
Verify with: php artisan route:list --name=user.direct-login -v
The route should show only these middleware:
- web
- LocaleSessionRedirect
- LaravelLocalizationRedirectFilter
- LaravelLocalizationViewPath
If you see Authenticate:web or other auth middleware, the route is in the wrong location.
Related Documentation
- SECURITY_OVERVIEW.md - Complete authentication system documentation
- Multi-Guard Authentication System
- Profile Switch Flow
- Session Security