Come for the Simplicity, Stay for the Extensibility: Identity with FusionAuth
Published on by Benjamen Pyle

As a developer, few things are more important than the security of my applications and APIs that I deliver to my customers. However, too many times, this security is often thought of too late in the design or I am forced to fall into the same old solutions that I've used before that require me to make decisions that I don't love, but live with. With that in mind, I'm always asking myself, are there other ways to solve common problems that make my solutions more resilient and extendable as the needs of the users on that system continues to evolve? In the PHP and Laravel world, that seems to always be the case. And when it comes to Auth, there's FusionAuth for this exact need.
In this article, I'm going to demonstrate an example of just how to leverage FusionAuth within a Laravel Application that is deployed on AWS' Elastic Container Service deployed with the Cloud Development Kit.
Disclosure
But before we begin and for disclosure, FusionAuth sponsored me to experiment with their product and report my findings. They have rented my attention, but not my opinion. Here is my unbiased view of my experience as a developer when building a Laravel application that uses FusionAuth for Authentication.
Solution Overview
When working through example solutions, I always like to start from the final output and then work backwards. This isn't just for the article's writing, but also when I'm putting together the outline of what I want to share with the running solution. There's so much choice when deploying Laravel applications, but my default cloud provider is AWS. I've been running high scale production workloads on their cloud for over 10 years and have a tremendous amount of comfort in the pieces needed to get this code deployed. However, a picture says a thousand words.
The key components of this architecture diagram to highlight are:
- A PHP Laravel application that has been containerized and fronted by Nginx
- Cloud hosted FusionAuth instance configured for OAuth callbacks to my application
- An AWS VPC consisting of
- Public Subnet
- Private Subnet
- NAT Gateway for the application in the private subnet to communicate with FusionAuth
With the design set, let's dive into the solution in the order listed above!
Solution Walkthrough
I'll quickly walk through the setup of the application and the project structure because I want to spend more time showing Socialite, configuring FusionAuth, and the AWS deployment.
Laravel Application Setup
I didn't do anything special when creating the Laravel application. I kicked off the build by running:
laravel new fusionauth-app
I then answered the setup questions by choosing PHPUnit
for testing, SQLite
for the database, and Vite for my frontend tooling. This setup yields a project structure that has the following directories and files.
From this project setup, it's time to add Socialite.
Laravel Socialite Connected to FusionAuth
Socialite defines itself as this:
Laravel Socialite provides an expressive, fluent interface to OAuth authentication with Bitbucket, Facebook, GitHub, GitLab, Google, LinkedIn, Slack, Twitch, and X. It handles almost all of the boilerplate social authentication code you are dreading writing. -- Socialite
Essentially, Socialite is an API to work with various OAuth providers. These could be social providers such as Facebook, Google, or GitHub. Or they could be my own provider that I use specifically inside my application like with FusionAuth. I'll get into the benefits of having my own OAuth provider when I dive into FusionAuth below. But, by installing Socialite into my Laravel application, I could potentially have my users authenticated with multiple providers while limiting their access in my application.
But for connecting with FusionAuth, how do I go about setting it up?
Step 1 involves adding Socialite to my project.
# Add the Socialite dependencycomposer require laravel/socialite# Add the FusionAuth providercomposer require socialiteproviders/fusionauth
Step 2 involves setting some environment variables in my .env
file and providing those values in my config/services.php
setup.
# .env file adds# FusionAuth Settings# OAuth requires a client id to be registeredFUSIONAUTH_CLIENT_ID=<OAuth client id set in FusionAuth># For the flow I've choosen, a secret is required registeredFUSIONAUTH_CLIENT_SECRET=<OAuth client secret set in FusionAuth># URL base where the FusionAuth API for me is hostedFUSIONAUTH_BASE_URL=<Base URL to FusionAuth. ex: https://your-name.fusionauth.io># A FusionAuth Tenant that I've configuredFUSIONAUTH_TENANT_ID=<FusionAuth Tenant ID># An OAuth required piece that tells the provider where to redirect the user# when successful. This also must exactly match the 'redirect_uri' request parameterinitially provided.# The provider will check thisFUSIONAUTH_REDIRECT_URI=<OAuth Client Redirect URL>
The variables are necessary as they will fund the services.php
array values that'll be used for the FusionAuth Socialite provider.
return [ 'fusionauth' => [ 'client_id' => env('FUSIONAUTH_CLIENT_ID'), 'client_secret' => env('FUSIONAUTH_CLIENT_SECRET'), 'redirect' => env('FUSIONAUTH_REDIRECT_URI'), 'base_url' => env('FUSIONAUTH_BASE_URL'), // Base URL of your cloud instance or self hosted instance 'tenant_id' => env('FUSIONAUTH_TENANT_ID'), // Tenant ID of the client (leave blank if you only have one) ]];
Step 3 is about providing routes for sending the user to FusionAuth as well as handling the callback that is specified in the URL above. I need to build a Controller
with two functions to handle these endpoints.
The first function performs the redirect to FusionAuth and is called redirectToFusionAuth
. That function takes the base URL that I defined above, plus the other elements to redirect the user to FusionAuth to sign in.
The second function callbackFusionAuth
handles the return from the FusionAuth server. This is the callback URL that must match in the FusionAuth server which I'll show in a bit to the environment variable that I set above. When this is called, I'm first going to fetch the user from the return payload. All of that is done by Socialite's FusionAuth API. From there, I'm storing the user in my local SQLite table and by pulling out pieces of the user via the $user
object. I'm lastly, logging in and then redirecting.
<?php namespace App\Http\Controllers; use App\Models\User;use Illuminate\Support\Facades\Auth;use Illuminate\Support\Facades\Log;use Laravel\Socialite\Facades\Socialite; class FusionAuthLoginController extends Controller{ public function redirectToFusionAuth() { return Socialite::driver('fusionauth')->redirect(); } public function callbackFusionAuth() { $user = Socialite::driver('fusionauth')->user(); $user = User::updateOrCreate([ 'fusionauth_id' => $user->id, ], [ 'name' => $user->name, 'email' => $user->email, 'given_name' => $user->user['given_name'], 'family_name' => $user->user['family_name'], 'middle_name' => $user->user['middle_name'], 'zoneinfo' => $user->user['zoneinfo'], 'birthdate' => $user->user['birthdate'], 'fusionauth_access_token' => $user->token, 'fusionauth_refresh_token' => $user->refreshToken, ]); Auth::login($user); return redirect('/profile'); }}
For reference, this is my User
class.
<?php 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; /* The attributes that are mass assignable. * * @var array<int, string> */ protected $fillable = [ 'name', 'email', 'birthdate', 'family_name', 'given_name', 'middle_name', 'zoneinfo', 'password', 'fusionauth_id', 'fusionauth_access_token', 'fusionauth_refresh_token', ]; /** * The attributes that should be hidden for serialization. * * @var array<int, string> * */ protected $hidden = [ 'password', 'remember_token', 'fusionauth_id', 'fusionauth_access_token', 'fusionauth_refresh_token', ]; /** * The attributes that should be cast. * * @var array<string, string> */ protected $casts = [ 'email_verified_at' => 'datetime', 'password' => 'hashed', ];}
What I really like about this approach is that once the User
is logged in, I can take advantage of page level or API level checks to make sure my user is capable of performing the operation they attempted to perform. I can do that in the normal Laravel way with Gates
or Policies
. For more on that, here's the Laravel documentation that covers those operations.
At this point, I have a Laravel application that has the Socialite plugin installed and it's successfully connected to FusionAuth. Time to explore what the FusionAuth side of the equation looks like.
FusionAuth (Hosted)
What I like about this graphic is that I can use FusionAuth whether I run it myself or if I choose to let them host it for me. In either case, there are plenty of options to make this happen. Run books and walkthroughs exist to launch FusionAuth in Kubernetes, run as EC2 Instances in AWS, or put it in Docker and run in ECS. For this article, I really wanted to focus on the developer experience of using FusionAuth and less on the hosting of the infrastructure so I opted to run in the FusionAuth Hosted Cloud.
One of the first things I love about FusionAuth is that it is a full featured OAuth 2.0 Provider while also being an OIDC Identity Provider. Those two things often get confused but they are two different things. Think of OAuth 2.0 as the authorization protocol and OIDC is used for authentication. One deals with who you say you are (OIDC) and the other is do you have access to perform the operation you are about to attempt (OAuth 2.0). This article can't possibly cover the two specifications, so please feel free to read up more on these two concepts. But when you are familiar with them, these settings will feel very familiar when looking at configuring applications.
This is the main Application screen in FusionAuth. It's where I've configured my custom Laravel application and provided the necessary settings. Notice in the Authorized Redirect URLs, I've got a localhost
which is great for testing. And I also have my full callback that is hosted behind TLS in AWS (blacked out of course).
Across the tab row are familiar settings if you've worked with OAuth and OIDC before. Here's a quick tour!
Key Endpoints
Scopes
FusionAuth really delivers when it comes to setting up options. There are provisions for customizing the JWT via Lambdas (not AWS Lambdas) in the JWT tab. SAML integration is available if that's something needed. And the ability for users to self register which includes customization to that form.
I can't possibly cover all of the magic that sits behind that left navigation menu, but I want to cover the reporting aspect. I find that with a lot of Identity and Authorization solutions, I get asked things like "how many active users do we have?". "What's the monthly login count?". "How many registrations have happened this month?". FusionAuth has all of that and more behind the Reports navigation menu.
At this point, I've got a FusionApp instance up and hosted. I have a Laravel application that is connected to FusionAuth via the Socialite framework. What's left is to get this application deployed and look into how it all comes together.
AWS and CDK
So my Laravel application is hosted in AWS. I customized the splash page just a little bit but here it is running in behind HTTPS and hosted on AWS' Elastic Container Service(ECS). This means I've dockerized it and deployed it somehow. Did I use Sail? CloudFormation? Let's find out!
Why CDK
There are a lot of options when it comes to deploying infrastructure and applications in AWS. I could have used native CloudFormation. Perhaps I could have gone with the popular Terraform? Pulumi is another contender. My preferred way though is to leverage the AWS Cloud Development Kit (CDK). It's available in several different languages, but I almost always choose to go with TypeScript.
I like that with CDK, I can work with classes that look like their CloudFormation representations and I'm generally on par with the options that CloudFormation natively provides me. I also have a good feel on its documentation because I'm so accustomed to the normal AWS docs, that again, it just feels similar and falls into my comfort zone.
I do however want to call out that the manual configuration that I did above on FusionAuth could have been done with Terraform. This is an interesting point to make too if I was going to build all of my AWS infrastructure in Terraform. I could provision my FusionAuth resources in Terraform. And I've got this single tool approach to building my infrastructure, deployment, and configuring my FusionAuth environment. A nice triple threat.
Deployment Walkthrough
Remember back up to the solution architecture at the top of the article? There are public and private subnets, a virtual private cloud (VPC), Application Load Balancer, NAT Gateway, ECS cluster, a Fargate instance. All of those things need to be provisioned. In order to accomplish all of this, I've laid resources out into two directories.
- app-infra/- infra/
The infra
directory contains the base level resources. Essentially all of the infrastructure outside of the Fargate service. Each of the resources that I'm building ultimately support the deployment of the Laravel application. Because after all, what's the point of the infrastructure without the application?
For instance, building the ECS cluster looks like this.
let cluster = new Cluster( scope, `EcsCluster`, { clusterName: `DemoCluster`, vpc: props.vpc, defaultCloudMapNamespace: { name: "highlands.local", useForServiceConnect: true, type: NamespaceType.HTTP, vpc: props.vpc } });
Instead of walking through each of the items in the project, the full working sample is available at the bottom of the article via GitHub. However, I do want to call out the parts that are involved at hosting an application in AWS' ECS. To accomplish deploying a containerized application in ECS, the following services are involved.
- AWS Elastic Container Registry (ECR) - this is the service that holds my PHP/Nginx Docker image that will be deployed
- AWS ECS Cluster - essentially is just a boundary for applications. More of an infrastructure and permissions boundary
- AWS ECS Service - the definition of my application as it relates to the Application Load Balancer, Deployment Controller, and parameters like how many instances of my application should run
- AWS ECS Task - this is the container level definition which points at ECR, allocates how much memory/CPU, defines log drivers, and Identity and Access Management policies at the AWS level for controlling what my application can do in the AWS ecosystem.
But to make all of this happen, I need to go back to the Laravel application. Like I've mentioned, the application is packaged up into a Docker container with an Nginx front door that acts as a reverse proxy for Laravel. Nginx does a great job at serving static content while forwarding dynamic content requests to Laravel to do its work. I find this to be a great pairing.
Inside of the project, there is a docker/
directory that holds an entrypoint.sh
which starts Nginx and runs PHP.
#!/usr/bin/env bash # Start Nginx serviceservice nginx start # Run Laravel migrationsphp artisan migrate --force # Create symbolic link for storagephp artisan storage:link # Clear and optimize the application cachephp artisan optimize:clearphp artisan optimize # Start PHP-FPMphp-fpm
Also inside of that directory is a default.conf
file which is the configuration file for Nginx.
server { listen 80 default_server; index index.php index.html; server_name localhost; error_log /var/log/nginx/error.log; access_log /var/log/nginx/access.log; root /var/www/public; location ~ \.php$ { fastcgi_split_path_info ^(.+\.php)(/.+)$; fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param PATH_INFO $fastcgi_path_info; fastcgi_hide_header X-Powered-By; # Tells PHP we're using a reverse proxy with TLS termination fastcgi_param HTTPS on; fastcgi_param HTTP_X_FORWARDED_PROTO $scheme; fastcgi_param HTTP_X_FORWARDED_SSL on; add_header Content-Security-Policy "upgrade-insecure-requests" always; } location / { try_files $uri $uri/ /index.php?$query_string; }}
AWS treats CloudFormation deployments as "Stacks" which hold resources to be provisioned. The two directories that hold the two CDK projects each create their own stack with its own resources to manage. Deploying them is as straightforward as:
cd infracdk deploy#cd ../app-infracdk deploy
The Final Product
Whew. That was quite the tour through Laravel and Socialite, FusionAuth, and then AWS CDK. But what did all of that work ultimately produce? I now have an application that can load from ECS, authenticate a user with FusionAuth, and then I've built a simple profile page to show the details I'm storing in FusionAuth. Those details are captured in this user record. This record holds my email, a birthdate, a first, middle, and last name, and what's not visible is the timezone I'm located in.
When I browse to my application and click on Login in the top right, I'm redirected by the
FusionAuthController
to this page where I can enter my credentials.
And when they are successfully validated, I'll be redirected to the
callback
route which will parse, save, and retrieve my user details, and then redirect me to this profile page.
Impressions and Takeaways
Authentication and authorization are parts of a solution that rarely ever shine in a good way. When things go wrong, and then it's less of a shine, and more of a glow. The glow of folks with pitchforks and torches heading toward you–not the happy kind of glow. So what I am looking for in a solution is reliability, configurability, and extensibility. While I haven't battle tested FusionAuth in a high-scale production environment, I hope you've seen from this article that it's very configurable and highly extensible. And anecdotally, I can tell from those pieces and the deployment options from my hosting, to FusionAuth's hosting, I believe it would hold up on reliability. I've run a handful of solutions throughout my career to solve this need, and I wouldn't hesitate to evaluate this against other providers.
From my working with FusionAuth, the pricing model also looks very attractive. And in addition, the support that can be had will suit needs from an application with a 24x7 requirement down to an internal application that just needs availability during working hours.
Things I liked
I liked the configurability quite a bit. The user experience from an administrator's standpoint was spot on. I found I could get to all of the pieces and parts of configuring OAuth and OIDC that I wanted. Again, from experience, there are a great deal of adjustments and settings that can be applied on compliant providers, and everything I wanted seemed to be in the right places.
Integration with PHP and Laravel was so simple. I literally did what I described in the article. I plugged in Socialite, set some variables, wrote a small Controller
and things were working. I didn't experience any hiccups or things that didn't work as I expected.
The last piece I really liked is an often overlooked part of a solution. The documentation. I didn't have more than topical knowledge of FusionAuth before this article, and I was able to get things up and running in a reasonable amount of time. I was also able to explore concepts like the Terraform bits, deployment options, and hosting that best fit the needs of this article.
Areas for Improvement
I don't think this is necessarily on FusionAuth as much as it is with what I was looking to achieve with this article. When I think about deploying PHP applications, I don't typically think containers and I surely don't think Kubernetes. My experiences with both of those platforms are much about deploying Microservices Architectures with a fair number of distributed API endpoints. When I'm doing PHP, I think more consolidation. The API and dynamic content are often served by the same process. Sure, horizontally scaled, but not distributed.
The FusionAuth documentation is amazing like I stated and has several walkthroughs on deploying with Kubernetes. It does require a database and an ElasticSearch instance which is where I sort of hit the pause on running things myself for this article. I personally, if deploying Laravel, would opt for the hosted route. I want to focus on my development and focus less on infrastructure. So while I did love the Helm charts and the description in the documentation about how to get FusionAuth hosted myself, I opted to go with them hosting it.
It would be a big boost if there were self contained and deployable templates. For AWS, those would be ready to run CloudFormation files/projects with the variables required to make things happen. Those templates would already have the necessary pieces and establish the ideal setup for hosting FusionAuth in AWS. Again, I'm nitpicking here though because I was more than satisfied with the hosted experience.
Wrapping Up
Rolling your own authentication is not something that people should be undertaking in this day and age. Now I know there are examples of applications where a forms based authentication might be good enough. However, if you think that extension of your user base is on the roadmap, investing in 3rd party solution is the way to go. There is plenty of competition in this space, but my experiences with FusionAuth during the duration of writing this article tells me that if I was building something from scratch, I'd be looking at FusionAuth for my Auth bits.
The product is easy to manage, highly configurable, and provides plenty of extension points to satisfy the use cases I've seen throughout my career. And when building with Laravel, plugging it into my application with Socialite was super simple. Combining it with AWS, I'm confident that it would be a rock solid powerhouse for my builds.
I reference a ton of code and alluded to snippets throughout this article, so as shared, here's the GitHub repository that you can clone, build, and deploy yourself.
Thanks for reading and happy building!

Benjamen Pyle is a technology executive and software developer with over 20 years of experience across startups and large enterprises. He is Co-Founder and CEO of Pyle Cloud Technologies.