Laravel Socialite SSO & AWS Cognito Tutorial

By James Siebert, Created on 28th Jan 2022

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

 

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, let's just use any external URL e.g https://google.com

Click on Domain name and set a URL prefix, this will set 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 Laravel's 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 sign-up 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.