Sign Up
Hero

Developing RESTful APIs with Lumen (A PHP Micro-framework)

Lumen is a PHP micro-framework built to deliver microservices and blazing fast APIs. Learn how to build and secure RESTful APIs with Lumen

This post has been updated to Lumen 7.x and Auth0 PHP SDK 7.x.

TL;DR: In this tutorial, I'll show you how easy it is to build and secure an API with Lumen. Check out the repo to get the code.


Lumen is an open-source PHP micro-framework created by Taylor Otwell as an alternative to Laravel to meet the demand of lightweight installations that are faster than existing PHP micro-frameworks such as Slim and Silex. With Lumen, you can build lightning-fast microservices and APIs that can support your Laravel applications.

Lumen Features and Architecture

Lumen utilizes the Illuminate components that power the Laravel framework. As such, Lumen is built to painlessly upgrade directly to Laravel when needed; for example, when you discover that you need more features out of the box than what Lumen offers.

These are some of the built-in features of Lumen:

  • Routing is provided out of the box in Lumen. This includes basic routing, routing parameters, named routes, and route groups such as middleware.

  • Authentication does not support session state. However, incoming requests are authenticated via stateless mechanisms such as tokens.

  • Caching is implemented the same as in Laravel. Cache drivers such as Database, Memcached, and Redis are supported. For example, you can install the illuminate/redis package via Composer to use a Redis cache with Lumen.

  • Errors and Logging are implemented via the Monolog library, which provides support for various log handlers.

  • Queuing services are similar to the ones offered by Laravel. A unified API is provided across a variety of different queue back-ends.

  • Events provide a simple observer implementation that allows you to subscribe and listen for events in your application.

  • Bootstrapping processes are located in a single file.

"Lumen is an amazing PHP micro-framework that offers a painless upgrade path to Laravel."

Tweet This

Lumen Key Requirements

To use Lumen, you need to have the following tools installed on your machine:

  • PHP: Make sure PHP >= 7.2 is installed on your machine. Furthermore, ensure that the following PHP extensions are installed. OpenSSL, PDO and Mbstring.
  • Composer: Navigate to the Composer website and install it on your machine. Composer is needed to install Lumen's dependencies.

Note: You'll need MySQL for this tutorial. Navigate to the MySQL website and install the community server edition. If you are using a Mac, I recommend following these instructions. For this tutorial, you can use MySQL straight from the terminal, but if you'd prefer a MySQL GUI, check out Sequel Pro for Mac or HeidiSQL for Windows.

Building a Fast Authors API Rapidly With Lumen

At Auth0, we have many technical writers, otherwise known as authors. A directive has been given to developing an app to manage Auth0 authors. The front-end app will be built with ReactJS. However, it needs to pull data from a source and also push to it. Yes, we need an API!

This is what we need the API to do:

  • Get all authors.
  • Get one author.
  • Add a new author.
  • Edit an author.
  • Delete an author.

Let's flesh out the possible endpoints for this API. Given some authors resource, we'll have the following endpoints:

  • Get all authors - GET /api/authors
  • Get one author - GET /api/authors/23
  • Create an author - POST /api/authors
  • Edit an author - PUT /api/authors/23
  • Delete an author - DELETE /api/authors/23

What will be the author attributes? Let's flesh it out like we did the endpoints.

  • Author: name, email, twitter, github, location, and latest_article_published.

Install Lumen

Run the following command in your terminal to create a new project with Lumen:

composer create-project --prefer-dist laravel/lumen authors

cd into the newly created project.

cd authors

Now, run php -S localhost:8000 -t public to serve the project. Head over to your browser. You should see the index page like so:

Authors Index

Activate Eloquent and Facades

As I mentioned earlier, the entire bootstrap process is located in a single file.

Open up the bootstrap/app.php and uncomment this line, // app->withEloquent. Once uncommented, Lumen hooks the Eloquent ORM with your database using the connections configured in the .env file.

Make sure you set the right details for your database in the .env file.

Next uncomment this line //$app->withFacades();, which allows us to make use of Facades in our project.

Setup Database, Models and Migrations

At the time of this writing, Lumen supports four database systems: MySQL, Postgres, SQLite, and SQL Server. We are making use of MySQL in this tutorial. First, we'll create a migration for the authors table.

Migrations are like version control for your database, allowing your team to easily modify and share the application's database schema.

Run the command below in the terminal to create the authors table migration:

php artisan make:migration create_authors_table

The new migration will be placed in your database/migrations directory. Each migration file name contains a timestamp, which allows Lumen to determine the order of the migrations. Next, we'll modify the recently created migration to include the attributes we need for the authors table.

Open up the migration file and modify it like so:

<?php

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

class CreateAuthorsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('authors', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name');
            $table->string('email');
            $table->string('github');
            $table->string('twitter');
            $table->string('location');
            $table->string('latest_article_published');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('authors');
    }
}

Here we're just adding a few extra columns to the authors table such as social handles, location, and a field for the last_article_published.

Now, go ahead and run the migration like so:

php artisan migrate

Check your database. You should now have the authors and migrations tables present.

Let's create the Author model. Create an app/Author.php file and add the code below to it:

app/Author.php

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Author extends Model
{

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name', 'email', 'github', 'twitter', 'location', 'latest_article_published'
    ];

    /**
     * The attributes excluded from the model's JSON form.
     *
     * @var array
     */
    protected $hidden = [];
}

In the code above, we made the author attributes mass assignable.

Set up Routes

Routing is fairly straight-forward. Open up routes/web.php and modify it like so:

<?php

/*
|--------------------------------------------------------------------------
| Application Routes
|--------------------------------------------------------------------------
|
| Here is where you can register all of the routes for an application.
| It is a breeze. Simply tell Lumen the URIs it should respond to
| and give it the Closure to call when that URI is requested.
|
*/

$router->get('/', function () use ($router) {
    return $router->app->version();
});

$router->group(['prefix' => 'api'], function () use ($router) {
  $router->get('authors',  ['uses' => 'AuthorController@showAllAuthors']);

  $router->get('authors/{id}', ['uses' => 'AuthorController@showOneAuthor']);

  $router->post('authors', ['uses' => 'AuthorController@create']);

  $router->delete('authors/{id}', ['uses' => 'AuthorController@delete']);

  $router->put('authors/{id}', ['uses' => 'AuthorController@update']);
});

In the code above, we have abstracted the functionality for each route into a controller, AuthorController. Route groups allow you to share route attributes, such as middleware or namespaces, across a large number of routes without needing to define those attributes on each route. Therefore, every route will have a prefix of /api. Next, let's create the Author Controller.

Set up Author Controller

Create a new file, AuthorController.php in app/Http/Controllers directory and add the following code to it like so:

<?php

namespace App\Http\Controllers;

use App\Author;
use Illuminate\Http\Request;

class AuthorController extends Controller
{
    
    public function showAllAuthors()
    {
        return response()->json(Author::all());
    }

    public function showOneAuthor($id)
    {
        return response()->json(Author::find($id));
    }

    public function create(Request $request)
    {
        $author = Author::create($request->all());

        return response()->json($author, 201);
    }

    public function update($id, Request $request)
    {
        $author = Author::findOrFail($id);
        $author->update($request->all());

        return response()->json($author, 200);
    }

    public function delete($id)
    {
        Author::findOrFail($id)->delete();
        return response('Deleted Successfully', 200);
    }
}

Let's analyze the code above. First, we have use App\Author, which allowed us to require the Author model that we created earlier.

Next, we've created the following five methods:

  • showAllAuthors
    • /GET
  • showOneAuthor
    • /GET
  • create
    • /POST
  • update
    • /PUT
  • delete
    • /DELETE

These will allow us to use that Author model to interact with author data. For example, if you make a POST request to /api/authors API endpoint, the create function will be invoked, and a new entry will be added to the authors table.

Author controller method overview:

  • showAllAuthors
    • checks for all the author resources
  • create
    • creates a new author resource
  • showOneAuthor
    • checks for a single author resource
  • update
    • checks if an author resource exists and allows the resource to be updated
  • delete
    • checks if an author resource exists and deletes it

Controller responses:

  • response()
    • global helper function that obtains an instance of the response factory
  • response()->json()
    • returns the response in JSON format.
  • 200
    • HTTP status code that indicates the request was successful.
  • 201
    • HTTP status code that indicates a new resource has just been created.
  • findOrFail
    • throws a ModelNotFoundException if no result is not found.

Finally, test the API routes with Postman.

Author POST operation - POST http://localhost:8000/api/authors

Make sure you have selected POST from the dropdown, and then you can fill the form data in by clicking on Body and then selecting form-data. Fill in a value for name, email, etc. to create a new author.

Author GET operation - GET http://localhost:8000/api/authors

You should now see an array of objects, including the author you just created plus any others in the database.

Author PUT operation

The PUT operation allows us to edit an existing author. Notice the author's id in the URL.

Author DELETE operation

Finally, we can delete a specific author as well.

Now we have a working API. Awesome!

Lumen API Validation

When developing applications, never trust the user. Always validate incoming data.

In Lumen, it's very easy to validate your application's incoming data. Lumen provides access to the $this->validate helper method from within Route closures.

Currently, in our API, we're not checking what people are sending through to our create method. Let's fix that now.

Open up the AuthorController file and modify the create method like this:

// ...
 public function create(Request $request)
    {
        $this->validate($request, [
            'name' => 'required',
            'email' => 'required|email|unique:authors',
            'location' => 'required|alpha'
        ]);

        $author = Author::create($request->all());

        return response()->json($author, 201);
    }
// ...

Now test the API POST route with Postman.

It validated the incoming requests and returned the appropriate error message.

  • name, email, and location were required. In testing the API, name and email were not provided.
  • email was required to be in email format.
  • location was required to be entirely alphabetic characters, alpha. Nothing more. Numbers were provided as the value for location.

Note: Always validate incoming data. Never trust your users!

Check out a plethora of validation rules that you can use with Lumen.

Securing the Authors API with Auth0

Right now, an application can make requests to any of the endpoints present in our API. In a real-world scenario, we would want to restrict our API so that only certain authorized users have the ability to do this. A few things need to happen here.

  1. A user signs in with their credentials (to prove who they are, i.e., authenticate)
  2. If the user is authorized to use the API, the application is issued an API access token
  3. Whenever an API request is made, the application will send that API access token along with the request
  4. If the access token is valid, the API will respond with the requested data

In this tutorial, we're going to focus on what happens in step 4 of that list (step 9-10 in the diagram). Since we're only building the backend API here, you'll need to create a separate front-end to accomplish the first two steps. Here is an awesome example of how you can do that using Auth0 with React.

For now, let's focus on generating access tokens using JSON Web Tokens.

JSON Web Token, commonly known as JWT, is an open standard for creating JSON-based access tokens that make some claim, usually authorizing a user or exchanging information. This technology has gained popularity over the past few years because it enables backends to accept requests simply by validating the contents of these JWTs.

JWTs can be used for authorization or information exchange. In this tutorial, we'll be using JWTs to grant authorization to applications (users) using our API.

Whenever the user wants to access a protected route or resource (an endpoint), the user agent must send the JWT, usually in the Authorization header using the Bearer schema, along with the request.

When the API receives a request with an access token, the first thing it needs to do is validate the token. If the validation fails, then the request must be rejected.

For more information about JSON Web Tokens, check out our free ebook below.

We will make use of Auth0 to issue our access tokens. With Auth0, we only have to write a few lines of code to get an in-depth identity management solution which includes:

If you haven't done so yet, this is a good time to sign up for a free Auth0 account.

Try out the most powerful authentication platform for free.Get started →

Once you have your Auth0 account, go ahead and create a new API in the dashboard. An API is an entity that represents an external resource, capable of accepting and responding to requests made by clients, such as the authors API we just made.

Auth0 offers a generous free tier to get started with modern authentication.

Login to your Auth0 management dashboard and create a new API client.

Click on the APIs menu item and then the Create API button.

Create a New API

Next, you will need to give your API a Name and an Identifier. The Name can be anything you choose, so make it as descriptive as you want. The Identifier will be used to specify your API and cannot be changed once set. We'll be using it as an audience later when configuring the access token verification.

Here's my setup for the author's API:

Once you have yours filled out, click on the Create API button.

Creating the Authors API

Next head over to your terminal and install the Auth0 PHP SDK in your project's root directory:

composer require auth0/auth0-php

Setting up environment variables

In this section, we're going to create the middleware to validate access tokens. The middleware will use some environment variables, so let's set those up first.

Open up .env and add the following:

AUTH0_DOMAIN=https://your-domain.auth0.com/
AUTH0_AUD=https://authorsapi.com

Replace these values with your own from the Auth0 dashboard.

AUTH0_DOMAIN

To find your domain, click on APIs > Authors API > Quick Start > PHP in the dashboard. Copy the value listed for authorized_iss and paste it into .env as AUTH0_DOMAIN.

When filling in the AUTH0_DOMAIN value, make sure you add the trailing slash: https://xyz.auth0.com/.

AUTH0_AUD

You can find this value in the Auth0 dashboard in the same place as the domain:

APIs > Quick Start > PHP.

Copy the value listed for valid_audiences and paste it in for AUTH0_AUD. If you followed the naming conventions of this tutorial, it would be https://authorsapi.com.

Create the Auth0 Middleware

Create a new middleware file, Auth0Middleware.php, in the app/Http/Middleware directory. Add the following code to it:

<?php

namespace App\Http\Middleware;

use Closure;
use Auth0\SDK\Exception\InvalidTokenException;
use Auth0\SDK\Helpers\JWKFetcher;
use Auth0\SDK\Helpers\Tokens\AsymmetricVerifier;
use Auth0\SDK\Helpers\Tokens\TokenVerifier;


class Auth0Middleware
{
    /**
     * Run the request filter.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        $token = $request->bearerToken();
        if(!$token) {
        return response()->json('No token provided', 401);
        }

        $this->validateToken($token);

        return $next($request);
    }

    public function validateToken($token)
    {
        try {
            $jwksUri = env('AUTH0_DOMAIN') . '.well-known/jwks.json';
            $jwksFetcher = new JWKFetcher(null, [ 'base_uri' => $jwksUri ]);
            $signatureVerifier = new AsymmetricVerifier($jwksFetcher);
            $tokenVerifier = new TokenVerifier(env('AUTH0_DOMAIN'), env('AUTH0_AUD'), $signatureVerifier);

            $decoded = $tokenVerifier->verify($token);
        }
        catch(InvalidTokenException $e) {
            throw $e;
        };
    }
}

This middleware checks if a request is made with a valid access token. In the next step, we'll apply this middleware to all of our routes that we want to protect. That way, before the request is executed, the middleware will run and check for the valid access token.

Let's look at how we validate the access token.

The handle() method first takes the request and grabs the access token. If it doesn't exist, an error is returned, and the request fails.

If the token does exist, we need to check that it's valid, which is done in validateToken().

First, we grab the JSON Web Key Set URI.

$jwksUri = env('AUTH0_DOMAIN') . '.well-known/jwks.json';

The JSON Web Key Set (JWKS) is a set of keys that contains the public keys used to verify any JSON Web Token (our access token) issued by the authorization server and signed using the RS256 signing algorithm.

If you're curious, you can find your public JWKS at the url: your Auth0 domain + '.well-known/jwks.json'. Use the domain you used in your .env file. Here's mine: https://demo-apps.auth0.com/.well-known/jwks.json.

Next, we're using JWKFetcher() to pull the keys from that URI. The first parameter accepts a cache handler to save the keys to the cache. We haven't set one up, so just leave it null.

$jwksFetcher = new JWKFetcher(null, [ 'base_uri' => $jwksUri ]);

Next up is the $signatureVerifier:

$signatureVerifier = new AsymmetricVerifier($jwksFetcher);

This is how our application will verify the signature of the JWT. Auth0 has a private key that generated the signature, so we have to use the public key to validate that the sender of the JWT is who they say they are.

Note: The AsymmetricVerifier() is used for the RS256 signing algorithm. If you're using HS256, then use SymmetricVerifier() and pass it the Auth0 client secret instead. You can find the client secret in your Auth0 dashboard.

E.g. $signature_verifier = new SymmetricVerifier(env('AUTH0_CLIENT_SECRET'));

After setting up the signature verification, we must now validate the rest of the token.

$tokenVerifier = new TokenVerifier(env('AUTH0_DOMAIN'), env('AUTH0_AUD'), $signatureVerifier);

This takes 3 parameters:

  • Auth0 domain — the token issuer
  • Auth0 audience — the API identifier
  • Signature verifier — the token signature verifier set up previously

It will verify that the token exists, the signature is verified, the token algorithm is supported, and all JWT claims are valid.

If all of this passes, the token is decoded and the middleware allows the HTTP request to execute.

Assign middleware to routes

Now that the middleware is set up, we need to add it to our routes. The first step is to assign the middleware a short-hand key in bootstrap/app.php file's call to the $app->routeMiddleware() method.

Go ahead and open up bootstrap/app.php and uncomment this line of code:

...
// $app->routeMiddleware([
//     'auth' => App\Http\Middleware\Authenticate::class,
// ]);
...

Once uncommented, replace the Authenticate::class with Auth0Middleware::class like so:

$app->routeMiddleware([
    'auth' => App\Http\Middleware\Auth0Middleware::class,
]);

This will allow us to use the Auth0Midddleware that we just created. And now we can use the middleware key in the route options array in the routes/web.php file like so:

...
$router->group(['prefix' => 'api', 'middleware' => 'auth'], function () use ($router) {
  $router->get('authors',  ['uses' => 'AuthorController@showAllAuthors']);

  $router->get('authors/{id}', ['uses' => 'AuthorController@showOneAuthor']);

  $router->post('authors', ['uses' => 'AuthorController@create']);

  $router->delete('authors/{id}', ['uses' => 'AuthorController@delete']);

  $router->put('authors/{id}', ['uses' => 'AuthorController@update']);
});

Now, if a request is made to any endpoint, it first runs the Auth0Middleware. If the request doesn't have a valid access token or no token at all, it returns an error. Let's try all of this out.

Accessing any endpoint without an authorization header

Accessing any endpoint without any token provided

Accessing any endpoint without a valid access token

Now, let's test it with a valid access token. Head over to the test tab of your newly created API on your Auth0 dashboard.

Grab the Access token from the Test tab

Grab the Access Token

Now use this access token in Postman by sending it as an Authorization header to make a POST request to api/authors endpoint.

Accessing the endpoint securely

It validates the access token and successfully makes the POST request.

Note: If you're getting a message that the token cannot be trusted, try adding a trailing slash to AUTH0_DOMAIN in .env, e.g., https://xyz.auth0.com/

Adding permissions

Currently, this single access token will allow an application to run any requests, as long as it has a valid token. You may want to eventually issue certain permissions with the access token. Let's try it out.

In the Auth0 dashboard, find the API we've been using and then click on Permissions. Create a new scope that will grant permission to create a new author (e.g., create:authors). Then add a short description of what that scope does and click "Add".

Now our API expects that when an application makes a request to create a new author, it must also send an access token that includes the create:authors scope. To check for this, we need to add middleware that checks the scope in the access token. Open up Auth0Middleware.php and replace it with:

// app/Http/Middleware/Auth0Middleware.php
<?php

namespace App\Http\Middleware;

use Closure;
use Auth0\SDK\Exception\InvalidTokenException;
use Auth0\SDK\Helpers\JWKFetcher;
use Auth0\SDK\Helpers\Tokens\AsymmetricVerifier;
use Auth0\SDK\Helpers\Tokens\TokenVerifier;


class Auth0Middleware
{
    /**
     * Run the request filter.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next, $scopeRequired = null)
    {
        $token = $request->bearerToken();

        if(!$token) {
            return response()->json('No token provided', 401);
        }

        $decodedToken = $this->validateAndDecode($token);

        if ($scopeRequired && !$this->tokenHasScope($decodedToken, $scopeRequired)) {
            return response()->json(['message' => 'Insufficient scope'], 403);
        }
        return $next($request);
    }

    public function validateAndDecode($token)
    {
        try {
            $jwksUri = env('AUTH0_DOMAIN') . '.well-known/jwks.json';
    
            $jwksFetcher = new JWKFetcher(null, [ 'base_uri' => $jwksUri ]);
            $signatureVerifier = new AsymmetricVerifier($jwksFetcher);
            $tokenVerifier = new TokenVerifier(env('AUTH0_DOMAIN'), env('AUTH0_AUD'), $signatureVerifier);

            return $tokenVerifier->verify($token);
        }
        catch(InvalidTokenException $e) {
            throw $e;
        };
    }

    /**
     * Check if a token has a specific scope.
     *
     * @param \stdClass $token - JWT access token to check.
     * @param string $scopeRequired - Scope to check for.
     *
     * @return bool
     */
    protected function tokenHasScope($token, $scopeRequired)
    {
        if (empty($token['scope'])) {
            return false;
        }

        $tokenScopes = explode(' ', $token['scope']);
        return in_array($scopeRequired, $tokenScopes);
    }

}

A few changes were made here.

The first thing to note is we added another parameter, scopeRequired, to the handle() method. This is set to null by default, since most of our routes won't require the create:author permission. Later in our routes, we'll specify when it is required.

// ...
public function handle($request, Closure $next, $scopeRequired = null) {
    // ...
}
// ...

Next, we get the token that was validated and decoded in validateAndDecode():

// ...
$decodedToken = $this->validateAndDecode($token);
// ...

Next, we check if the scope is required for this request. If it is, we use that decoded token to check if the scope exists. If so, the request continues, but if not, we send an Insufficient scope message instead.

if ($scopeRequired && !$this->tokenHasScope($decodedToken, $scopeRequired)) {
    return response()->json(['message' => 'Insufficient scope'], 403);
}

We also added the method tokenHasScope(), which is what's doing the check for that specific scope in the previous if statement.

protected function tokenHasScope($token, $scopeRequired)
{
    if (empty($token['scope'])) {
        return false;
    }

    $tokenScopes = explode(' ', $token['scope']);
    return in_array($scopeRequired, $tokenScopes);
    }

It grabs all of the scopes in the token, splits each scope as an array item, and searches the array for the required scope.

Finally, we need to add this scope check middleware to our route for creating a new author. Open up routes/web.php and modify the post route as follows:

// routes/web.php
// ...
$router->group(['prefix' => 'api', 'middleware' => 'auth'], function () use ($router) {

    // ...

    $router->post('authors', ['middleware' => 'auth:create:authors', 'uses' => 'AuthorController@create']);

    // ...
});

Now, if you try to create a new author in Postman using that same token as before, you'll receive the "Insufficient scope" message.

To test that it works, make sure you're on the page with your API in the Auth0 dashboard and then go to the Permissions tab. Click on Machine to Machine Applications and find the API Application you've been using. Make sure the Authorized toggle is on and then click on the arrow. Now select the create:authors scope and press "Update".

Now the permission for create:authors has been added to our test token. Head back over to the "Test" tab and press "Copy token" to get the updated one.

Paste that token into the Authorization header as you did before (make sure you have Bearer before it), try the POST request again, and now it should have worked!

If you'd like to see what the decoded access token looks like, just add dd($decodedToken); inside the handle() method in app/Http/Middleware/Auth0Middleware.php right after the $decodedToken variable is declared. Then just run that POST request one more time in Postman, and you'll see the contents of the token, including the scope. Pretty cool! Just make sure you delete that test line in a real application.

Adding a front-end

This is just an example of how to create the API access tokens. Once you're ready to actually issue and use them, you need to create a front-end. Here are some amazing React and Vue.js authentication tutorials that cover how you can accomplish that.

Conclusion

Well done! You have learned how to build a rest API with the powerful PHP micro-framework Lumen and secure it using JWTs. Need to use PHP to build your API or micro-service? I'd bet on Lumen as the tool of choice for speed and ease of use.

As you've seen, Auth0 can help secure your API with ease. Auth0 provides more than just username-password authentication. It provides features like multifactor auth, breached password detection, anomaly detection, enterprise federation, single sign-on (SSO), and more.

Sign up

today so you can take the stress out of authentication and instead focus on building unique features for your app.

Please, let me know if you have any questions in the comment section. 😊