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();
}
}
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;
}
}
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
}
};
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
And I posted on my typical controller, olde traditional stuff:
Route::post('/somepath',function(){
// Stuff submission here
})->name('myroute');
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);
}
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'),
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();
});
The:
$table->foreignId('user_id')->nullable()->index();
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();
});
Conclustion
- 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
- If you change the type of primary key at
users
table also ensure you change the type insessions
table as well.
Top comments (0)