Integrating LinkedIn with Laravel & Filament: A Seamless OAuth Journey
Imagine giving your users a one-click way to enrich their profiles or share content without ever leaving your application. Manual data entry is tedious, but third-party integrations, particularly via OAuth, can seem daunting.
In our landing project, we aimed to simplify user profile management and content sharing by enabling direct LinkedIn account integration. The goal was a smooth, secure, and intuitive process for users to connect and disconnect their LinkedIn profiles.
The Integration Challenge
Integrating a service like LinkedIn involves several moving parts: redirecting users for authentication, handling callback responses, securely storing credentials, and providing a clear user interface to manage the connection. This isn't just about showing a button; it's about a robust, secure flow.
Building the Bridge: Key Components
Our approach focused on segmenting the logic into distinct, manageable components:
1. The OAuth Orchestrator: LinkedinConnectionController
This controller is the brain of the operation, handling the initial redirect to LinkedIn and processing the callback after a user authorizes our application. It orchestrates the OAuth dance, ensuring tokens are exchanged securely and user data is retrieved.
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Laravel\Socialite\Facades\Socialite; // Or a custom LinkedIn client
class LinkedinConnectionController extends Controller
{
public function redirectToLinkedIn()
{
return Socialite::driver('linkedin-openid')
->scopes(['r_liteprofile', 'r_emailaddress', 'w_member_social'])
->redirect();
}
public function handleLinkedInCallback()
{
try {
$user = Socialite::driver('linkedin-openid')->user();
// Store user's LinkedIn ID and token
auth()->user()->update([
'linkedin_id' => $user->getId(),
'linkedin_token' => $user->token,
// ... other profile data
]);
return redirect('/dashboard')->with('status', 'LinkedIn connected successfully!');
} catch (\Exception $e) {
return redirect('/dashboard')->with('error', 'Failed to connect LinkedIn: ' . $e->getMessage());
}
}
public function disconnectLinkedIn()
{
auth()->user()->update([
'linkedin_id' => null,
'linkedin_token' => null,
]);
return redirect('/dashboard')->with('status', 'LinkedIn disconnected.');
}
}
2. Database Foundation: User Table Updates
To persist the connection, we introduced new columns to our users table. This is crucial for linking a local user account to their LinkedIn profile and storing the necessary access tokens.
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->string('linkedin_id')->nullable()->after('email');
$table->string('linkedin_token', 1024)->nullable()->after('linkedin_id'); // Using a longer token field
$table->timestamp('linkedin_connected_at')->nullable()->after('linkedin_token');
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn(['linkedin_id', 'linkedin_token', 'linkedin_connected_at']);
});
}
};
3. User Experience: ConnectLinkedinWidget for Filament
Filament, our administrative panel framework, provided an elegant way to expose this functionality to administrators or directly to users within their dashboard. The ConnectLinkedinWidget displays the current connection status and offers "Connect" or "Disconnect" actions. This brings the backend logic to life in an intuitive UI.
<?php
namespace App\Filament\Widgets;
use Filament\Widgets\Widget;
use Illuminate\Contracts\View\View;
class ConnectLinkedinWidget extends Widget
{
protected static string $view = 'filament.widgets.connect-linkedin-widget';
public function isConnected(): bool
{
return (bool) auth()->user()->linkedin_id;
}
public function render(): View
{
return view(static::$view, [
'isConnected' => $this->isConnected(),
]);
}
}
And in the corresponding Blade view resources/views/filament/widgets/connect-linkedin-widget.blade.php:
<div>
@if ($isConnected)
<p>LinkedIn connected!</p>
<a href="{{ route('linkedin.disconnect') }}" class="button">Disconnect</a>
@else
<p>Connect your LinkedIn account.</p>
<a href="{{ route('linkedin.connect') }}" class="button">Connect with LinkedIn</a>
@endif
</div>
4. Routing and Testing
Robust routing ensures all OAuth redirects, callbacks, and disconnection requests are correctly handled. Extensive testing, leveraging tools like PHPUnit, guarantees the flow works as expected, covering success scenarios, error handling, and security aspects.
// In web.php
use App\Http\Controllers\LinkedinConnectionController;
Route::get('/linkedin/connect', [LinkedinConnectionController::class, 'redirectToLinkedIn'])->name('linkedin.connect');
Route::get('/linkedin/callback', [LinkedinConnectionController::class, 'handleLinkedInCallback'])->name('linkedin.callback');
Route::get('/linkedin/disconnect', [LinkedinConnectionController::class, 'disconnectLinkedIn'])->name('linkedin.disconnect');
Outcome and Takeaways
By modularizing the integration, from controller logic to database schema and UI components, we created a feature that is both powerful and easy to maintain. Users now enjoy a seamless experience, and the codebase remains clean and testable.
This structured approach to third-party integration, leveraging Laravel's capabilities and Filament's extensibility, minimizes complexity and maximizes developer efficiency. When tackling similar integrations, remember to break down the flow, consider all user journeys, and test rigorously.