In this tutorial, I will be demonstrating how to set up a basic Laravel project with the default authentication applied using Laravel Socialite. Then I will show you how to enable Socialite to authenticate users using an AWS Cognito user pool using OAuth2.
This project came about whilst I was trying to allow the sharing of a single user account between multiple apps.
I built an app using AWS Amplify with an existing Cognito user pool and wanted a way for a Laravel app to also access my existing user pool.
This method enables SSO, this allows the user to log in on one app, then if they visit another app using the same pool they are logged into the new app with just one login click, no username or password required.
Laravel Framework v8.61.0
Requirements:
Assuming some very basic knowledge in Laravel and AWS.
Composer (terminal: composer -v) – I am using 2.0.9
Laravel Installer (terminal: laravel -v) – I am using 4.2.8
PHP (terminal: php -v) – I am using 7.4.14
An AWS account.
I am using Windows but most of this is the same for Mac.
Create Laravel Project
I have a “laragon” parent projects folder setup on my local pc. This is where I install all of my Laravel projects, but you can do whatever works for you.
Windows Command Prompt:
cd C:\laragon\www\ laravel new socialite-cognito-example
Close the terminal when finished.
From here on we will be working in your IDE of choice, I use PhpStorm but Visual Studio Code is just as good and free.
Create a Database
Firstly we need to set up an empty database, I use Laragon for this but you can use anything you like.
Laragon uses phpMyAdmin to manage its databases.
Database name: socialite_cognito_example
Collation: utf8_general_ci
* Be careful to check the name phpMyAdmin generally uses “_” not “-“.
Add your values to the .env file
Path: .env
DB_DATABASE=socialite_cognito_example DB_USERNAME=root DB_PASSWORD=
This allows us to control which port our app is served on, this is useful when serving multiple apps at the same time.
SERVER_PORT=8001
This allows us to control which port our app is served on, this is useful when serving multiple apps at the same time.
COGNITO_HOST=https://your_cognito_domain.auth.your_region.amazoncognito.com COGNITO_CLIENT_ID=abc123 COGNITO_CLIENT_SECRET=aaabbbccc111222333 COGNITO_CALLBACK_URL=https://your-app.au.ngrok.io/oauth2/callback COGNITO_SIGN_OUT_URL=https://logout-redirect-to-site.com COGNITO_LOGIN_SCOPE="openid,profile"
Modify App Service Provider
Path: app/Providers/AppServiceProvider.php
use Illuminate\Support\Facades\Schema; public function boot() { Schema::defaultStringLength(125); }
Install dependencies
composer require laravel/ui composer require laravel/socialite
Socialite Providers provides us with an easy way to connect to any OAuth API. They also allow developers to create new integrations like the Cognito provider below which I am in the process of submitting.
This uses a “Socialite Manager” to handle the bulk of the work and the developers just need to create a new provider for the specific API connection, this keeps everything clean and uniform.
I learned most of this through implementing the Laravel Passport Provider which allows you to create your own Laravel based OAuth server and manage your own users.
composer require socialiteproviders/cognito
If you need to add or modify a local composer package check out this link
Add an event listener
Path : app/Providers/EventServiceProvider
Add this to the array:
protected $listen = [ ... \SocialiteProviders\Manager\SocialiteWasCalled::class => [ // add your listeners (aka providers) here 'SocialiteProviders\\Cognito\\CognitoExtendSocialite@handle', ], ];
Add cognito configuration
Path: config/services.php
'cognito' => [ 'host' => env('COGNITO_HOST'), 'client_id' => env('COGNITO_CLIENT_ID'), 'client_secret' => env('COGNITO_CLIENT_SECRET'), 'redirect' => env('COGNITO_CALLBACK_URL'), 'scope' => explode(",", env('COGNITO_LOGIN_SCOPE')), 'logout_uri' => env('COGNITO_SIGN_OUT_URL') ],
Install default Auth UI
php artisan ui bootstrap --auth
Edit Login View
Path: resources/views/auth/login.blade.php
Comment out the existing form and add this:
<div class="form-group row mb-0 mt-3"> <div class="col-md-8 offset-md-4"> <a href="{{ url('/oauth2/login') }}" class="btn btn-warning">Cognito Login</a> </div> </div>
Add logout buttons
Path: resources/views/home.blade.php
Add this in the card body
<h2>Home - User Dashboard</h2> <div class="form-group row mb-0 mt-3"> <div class="col-md-8 offset-md-4"> <a href="{{ url('/oauth2/logout') }}" class="btn btn-warning">Cognito Logout</a> </div> </div> <div class="form-group row mb-0 mt-3"> <div class="col-md-8 offset-md-4"> <a href="{{ url('/oauth2/switch-account') }}" class="btn btn-warning">Switch Account</a> </div> </div>
Modify NavBar Links
Path: resources/views/layouts/app.blade.php
Comment out the existing right ‘ul’ section and replace it with:
<!-- Right Side Of Navbar --> <ul class="navbar-nav ml-auto"> @guest <li class="nav-item"> <a href="{{ url('/oauth2/login') }}" class="nav-link">Cognito Login / Register</a> </li> @else <li class="nav-item dropdown"> <a id="navbarDropdown" class="nav-link dropdown-toggle" href="#" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" v-pre> {{ Auth::user()->first_name }} </a> <div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown"> <a href="{{ url('/oauth2/logout') }}" class="dropdown-item">Cognito Logout</a> <a href="{{ url('/oauth2/switch-account') }}" class="dropdown-item">Switch Account</a> </div> </li> @endguest </ul>
Modify welcome view links
Path: resources/views/welcome.blade.php
Comment out the @if (Route::has(‘login’)) section and paste this in:
<div class="hidden fixed top-0 right-0 px-6 py-4 sm:block"> @auth <a href="{{ url('/home') }}" class="text-sm text-gray-700 dark:text-gray-500 underline">Dashboard</a> @else <a href="{{ url('/oauth2/login') }}" class="text-sm text-gray-700 dark:text-gray-500 underline">Login</a> @endauth </div>
Modify default user model
Path: app/Models/User.php
protected $fillable = [ 'first_name', 'last_name', 'email', 'password', 'provider', 'provider_id', ];
Path: database/migrations/…_create_users_table.php
Schema::create('users', function (Blueprint $table) { $table->id(); $table->string('first_name'); $table->string('last_name'); $table->string('email'); $table->timestamp('email_verified_at')->nullable(); $table->string('password')->nullable(); $table->string('provider'); $table->string('provider_id'); $table->rememberToken(); $table->timestamps(); });
Run migration
php artisan migrate
Compile assets
npm install && npm run dev
* If you receive an error run it again.
Add Auth Routes
Path: routes/web.php
Replace everything with:
<?php use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Route; Route::get('/', function () { return view('welcome'); })->name('welcome'); Auth::routes(); Route::get('/home', [App\Http\Controllers\HomeController::class, 'index'])->name('home'); // OAuth (Cognito) Route::get('oauth2/login', [App\Http\Controllers\Auth\LoginController::class, 'redirectToExternalAuthServer']); // Login button - Post to OAuth Server Route::get('oauth2/callback', [App\Http\Controllers\Auth\LoginController::class, 'handleExternalAuthCallback']); // For OAuth2 Callback (Cognito) Route::get('oauth2/logout', [App\Http\Controllers\Auth\LoginController::class, 'cognitoLogout'])->name('oauth-logout'); // OAuth2 triggered logout (Cognito) Route::get('oauth2/switch-account', [App\Http\Controllers\Auth\LoginController::class, 'cognitoSwitchAccount'])->name('oauth-switch-account'); // Logout and login to another account
Login Controller
Path: app/Http/Controllers/Auth/LoginController.php
Replace everything with:
<?php namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; use App\Models\User; use App\Providers\RouteServiceProvider; use Illuminate\Foundation\Auth\AuthenticatesUsers; use Illuminate\Http\RedirectResponse; use Illuminate\Support\Facades\Auth; use Laravel\Socialite\Facades\Socialite; class LoginController extends Controller { use AuthenticatesUsers; // Where to redirect users after login. protected $redirectTo = RouteServiceProvider::HOME; public function __construct() { // guest only except logout functions $this->middleware('guest')->except('logout', 'cognitoLogout', 'cognitoSwitchAccount'); } // POST to Cognito Host // Example COGNITO_HOST/login?client_id=CLIENT_ID&response_type=code&scope=aws.cognito.signin.user.admin+email+openid+phone+profile&redirect_uri=CALLBACK_URL public function redirectToExternalAuthServer(): \Symfony\Component\HttpFoundation\RedirectResponse { return Socialite::driver('cognito')->redirect(); } // Callback from AWS Cognito // Example: http://myapp.ngrok.io/cognito/callback?code=1234&state=abc public function handleExternalAuthCallback(): RedirectResponse { // Override default scopes if needed $scopes = config('services.cognito.scope'); $user = Socialite::driver('cognito')->scopes($scopes)->stateless()->user(); // NOTE STATELESS - https://stackoverflow.com/questions/30660847/laravel-socialite-invalidstateexception //dd($user); // Show the available user attributes $authUser = $this->findOrCreateUser($user, 'cognito'); Auth::login($authUser, true); return redirect()->route('home'); } // If a user has registered before using social auth, return the user else, create a new user public function findOrCreateUser($user, $provider): User { // Search DB for a user with the provider_id = cognito user sub $authUser = User::where('provider_id', $user->user['sub'])->first(); if ($authUser) { // User found return $authUser; } // Access user profile data in cognito user $passportUser = $user->user; /* EXAMPLE COGNITO USER PROFILE "sub" => "88889999-2222-0000-1111-222111110000" // Subject - Cognito UUID of the authenticated user "birthdate" => "some_string" "email_verified" => "true" "gender" => "some gender string" "phone_number_verified" => "false" "phone_number" => "+61402172740" "given_name" => "FirstName" "family_name" => "LastName" "email" => "example@example.com" "username" => "88889999-2222-0000-1111-222111110000" */ // Create new local user return User::create([ 'first_name' => $passportUser['given_name'], 'last_name' => $passportUser['family_name'], 'email' => $passportUser['email'], 'provider' => $provider, 'provider_id' => $passportUser['sub'] ]); } // Logout of cognito, logout of app, redirect to specified logout url // Notes: Must be SSL, cognito and env sign out url must match. Ngrok has issues here so I use an external url instead. public function cognitoLogout(){ // Log out app Auth::logout(); // Call cognito logout url return Redirect(Socialite::driver('cognito')->logoutCognitoUser()); } // Logout of cognito, logout of app, redirect to cognito login. // Notes: Must be SSL, cognito and env redirect url must match. Use Ngrok for dev SSL simulation. public function cognitoSwitchAccount(){ // Log out app Auth::logout(); // Override default scopes if needed $scopes = explode(",", env('COGNITO_LOGIN_SCOPE')); // Call cognito logout url return Redirect(Socialite::driver('cognito')->scopes($scopes)->switchCognitoUser()); } }
Setup AWS Cognito User Pools
login to your AWS console
Open the Amazon Cognito service
Manage User Pools
Either create a new user pool or select an existing one.
Create a new client app or use an existing one.
Get ‘App client id’ & ‘App client secret’
General settings > App Clients

Local testing using SSL (ngrok)
The URLs you specify in your Cognito app must be SSL and as far as I know, ‘php artisan serve’ doesn’t easily support this.
As a good workaround, I use ngrok which serves your local app over HTTPS online with a URL.
There is a free version but for $5/mth they can provide you with a custom domain name that doesn’t change. Without this you have to update your .env and Cognito callback URL values each time you restart ngrok.
I placed the ngrok executable on my C:\
** Note: This works for everything except the logout URL, for testing I just use an external URL instead.
Using free version:
php artisan serve -- New Terminal Window -- cd c:\ ngrok http localhost:8001
Using paid version:
This SSL URL will be part of your callback URL.
Callback URL example: https://your-app.au.ngrok.io/oauth2/callback
php artisan serve -- New Terminal Window -- cd c:\ ngrok http --region=au --hostname=your-app-name.au.ngrok.io 8001
Set callback & callback URLs
This image also shows where to set your auth URL prefix, this is where you get your COGNITO_HOST URL.
The Callback URL must be the same as the COGNITO_CALLBACK_URL in your .env
The Sign out URL must be the same as the COGNITO_SIGN_OUT_URL in your .env, for now lets just use any external URL e.g https://google.com
Click on Domain name and set a URL prefix, this will he your Auth host URL.
You can test your login UI by clicking the ‘Launch Hosted UI’ link on the app client settings page.

Update environmental variables
COGNITO_HOST=<FROM DOMAIN PAGE> COGNITO_CLIENT_ID=<FROM APP CLIENTS PAGE> COGNITO_CLIENT_SECRET=<FROM APP CLIENTS PAGE> COGNITO_CALLBACK_URL=<SAME AS APP CLIENT SETTINGS> COGNITO_SIGN_OUT_URL=<SAME AS APP CLIENT SETTINGS> COGNITO_LOGIN_SCOPE="openid,profile"
Restart the local server
php artisan serve
Finished example
Welcome Page
The login link will redirect the user to the Cognito hosted sign-in UI (COGNITO_HOST)

AWS Cognito hosted login UI
The user signs into their User Pool account.
** The UI styling here is me just messing around with the UI options.

Sign up form
If the user doesn’t have an account they can sign up here. These fields are the required fields that were set when you created the Cognito user pool.
** The UI styling here is me just messing around with options.

User Dashboard
This is shown when a user is logged in.
FYI when a user logs in a user is added to Laravels user table (see Login Controller).
The Cognito Logout button logs the user out of the app and the user’s Cognito account then redirects the user to the logout URL you specified.
The Switch Account button logs the user out of the Laravel app and the Cognito account then redirects the user back to the Cognito hosted sign-in UI.

Future issues:
.htaccess
You may need to set up your .htaccess file when you actually deploy this on a server. This will force the site to look inside the public folder and serve the Laravel application properly.
AWS signup form validation
The AWS form seems pretty locked down and doesn’t give the level of data validation I would like. I am considering replacing this with my own form.