DEV Community

Dimitrios Desyllas
Dimitrios Desyllas

Posted on

Implications of using Aws cognito in Laravel 11 and the pool's user_id as application User Id.

I am implementing a system that has a system in Django and one user management panel in Laravel, later this may or may not be a full-fledged backoffice (current situation is uknown and I have no further specs).

Therefore, I opted for AWS cognito the reason why is because I wanted a unique user_id reference both in Laravel app and in Django system. The Django System was implemented first and uses a MongoDb for its data storage.

Cognito offered to me a unique way of managing the authentication.

Upon Laravel Side I used the laravel/socialite and socialiteproviders/cognito. But here are some quircks I needed to resolve:

Quirk 1 User Should ALWAYS reference upon DB

In my case I needed to go slow and just the panel to manipulate the data that exists Upon aws cognito. NOPE I had trouble to use default session provider with my own custom User model that references the one upon aws cognito.

In my case I just wanted to have a simple frotnent that was using the AWS api for cognito and manipulate the data once an Admin user is logged in. As explained above this is not Feasible.

In the end I made this controller:

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use App\Models\User;
use Aws\CognitoIdentityProvider\CognitoIdentityProviderClient;
use Laravel\Socialite\Facades\Socialite;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Redirect;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use League\Flysystem\Config;

class UserController extends Controller
{

    // This is my login handler
    public function login(Request $request)
    {
        $loggedin = Auth::check();
        $requestHasCode = $request->has('code');
        if(!$requestHasCode) {
            if($loggedin){
                // User is already authenticated redirect
                return $this->authRedirect();
            }

            // Logout prompts user back to login screen
            return $this->logout($request);
        }

        $socialiteUser = null;
        try {
            $socialiteUser = Socialite::driver('cognito')->stateless()->user();
        } catch (\Exception $e) {
            return Socialite::driver('cognito')->redirect();
        }

        if($socialiteUser != null){
            $user = User::createBySocialiteUser($socialiteUser);
            Auth::login($user,true);
            return $this->authRedirect();
        }

        return Socialite::driver('cognito')->redirect();
    }
}
Enter fullscreen mode Exit fullscreen mode

And upon default user model App\Models\User I did:

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class User extends Authenticatable
{
    use HasFactory, Notifiable;

    const USER_ADMIN='ADMIN';
    const USER_CLIENT='CLIENT';

    // Read bellow in the article regarding thisma
    public $incrementing = false;


 public static function createBySocialiteUser (\SocialiteProviders\Manager\OAuth2\User $user): ?self
    {
        $dataToUpdate = [
            'email' => $user->user['email'],
            'id' => $user->user['sub'], // Ensure this is the correct value
            'name' => $user->name ?? "Unknown User",
        ];

        // Use the correct key for the first argument
        $user=User::firstOrNew(['id' => $dataToUpdate['id']], $dataToUpdate);
        // First or New Does mto set the USer Id
        $user->id = $dataToUpdate['id'];
        $user->save();

        return $user;
    }
}
Enter fullscreen mode Exit fullscreen mode

As you can see I map the user sub as user Id.

In order to do this upon migration I set the id as string, the project was brand new and yet to be deployed at any environment (dev, staging or production). Thus I modified the migration directly:


<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('users', function (Blueprint $table) {
            $table->string('id', 36)->primary();
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password')->nullable();
            $table->enum('role', ['ADMIN', 'CLIENT'])->default('CLIENT');
            $table->rememberToken();
            $table->timestamps();
        });

        // Rest of migration goes here
    }
};

Enter fullscreen mode Exit fullscreen mode

The migration above caused me yet another implication

Quirk 2 Session and CSRF invalidation

At this point I was sure that everithin AOK but guess what, NOPE. Lemme explain:

I made a simple form with the typical csrf thing at my blade view:

@extends('layout.somelayout')

@section('main')

<form method="POST" action="{{route('myroute')}}">
            @csrf
            <!-- some extra fields here -->
            <div class="mt-1">
                <button type="submit" class="btn btn-primary" >Save</button>
            </div>
</form>
@endsection
Enter fullscreen mode Exit fullscreen mode

And I posted on my typical controller, olde traditional stuff:

Route::post('/somepath',function(){
 // Stuff submission here
})->name('myroute');
Enter fullscreen mode Exit fullscreen mode

But routed caused returning 419. I debugged the vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/VerifyCsrfToken.php directly. What I find out that every time I submitted the form a NEW csrf token was created upon:

protected function tokensMatch($request)
{

    $token = $this->getTokenFromRequest($request);

    return is_string($request->session()->token()) &&
               is_string($token) &&
               hash_equals($request->session()->token(), $token);
}
Enter fullscreen mode Exit fullscreen mode

The reason is because I used database as my session driver a good balance between scalability and not needing to deploy extra stuff in my stack. In my case I used the default table because I had no reason to change it:

    'table' => env('SESSION_TABLE', 'sessions'),
Enter fullscreen mode Exit fullscreen mode

But the migration for this table contained user id as big integer:

 Schema::create('sessions', function (Blueprint $table) {
            $table->string('id')->primary();
            $table->foreignId('user_id')->nullable()->index();
            $table->string('ip_address', 45)->nullable();
            $table->text('user_agent')->nullable();
            $table->longText('payload');
            $table->integer('last_activity')->index();
        });
Enter fullscreen mode Exit fullscreen mode

The:

 $table->foreignId('user_id')->nullable()->index();
Enter fullscreen mode Exit fullscreen mode

Created user_id at the table as Big integer, once user was logged in sucessfully the session failed to associate with the user_id meaning that upon each submission a new csrf token was generated.

The fix was to make the user_id into a string:


        Schema::create('sessions', function (Blueprint $table) {
            $table->string('id')->primary();
            $table->string('user_id',36)->nullable()->index();
            $table->string('ip_address', 45)->nullable();
            $table->text('user_agent')->nullable();
            $table->longText('payload');
            $table->integer('last_activity')->index();
        });
Enter fullscreen mode Exit fullscreen mode

Conclustion

  1. If OAuth authentication is used either bypass (ie. not use and if needed make your own implementation) the default guards OR set the loggedin user info upon DB
  2. If you change the type of primary key at users table also ensure you change the type in sessions table as well.

Top comments (0)