Deploy a Laravel Token API on EC2

Deploy a Laravel token API on EC2 and proxy it through API Gateway.

Introduction

30 Second Summary

Signing in to an app feels like one quick step. Behind that moment, the app has to reject the wrong person while letting the right person reach private information.

In this project, you will deploy a Laravel login API on an Amazon EC2 server. You will prove its Laravel Sanctum token flow from an intentional unauthenticated rejection to a protected profile request through an Amazon API Gateway HTTPS endpoint.

What You'll Build

From Windows PowerShell, you'll demonstrate a public HTTPS login flow from disposable credentials to a protected demo profile.

By the end of this project, you'll have:

  • A reachable Laravel health route that proves your application can boot successfully on its public server.
  • A complete authentication test where a tokenless profile request produces 401 Unauthorized. Disposable demo credentials produce a bearer token. That token reveals only the demo user's ID, name, and email.
  • The same login and profile flow working through a public Amazon API Gateway HTTPS URL.
  • Secret Mission: Add /api/logout to revoke only the token used for the request. Reusing that token must produce 401 Unauthorized.

Are there any prerequisites?

You'll need an AWS account with permission to create Amazon EC2 and Amazon API Gateway resources. Windows PowerShell and OpenSSH handle the connection and API tests.

Use only disposable demo credentials because the EC2 origin uses public HTTP.

Before We Start

Before any cloud resources exist, this checkpoint confirms your AWS Management Console access. Because the public EC2 origin uses HTTP, your disposable learning environment uses only demo credentials.

Launch and Prepare the EC2 Server

Your Laravel API needs a public Linux host before it has anywhere to run. Amazon EC2 provides the server that Laravel reaches later in the project.

This step launches Amazon Linux 2023 behind a security group. You will connect through SSH. You will prepare Apache HTTP Server, PHP, Composer, and MariaDB for the Laravel deployment.

In this step, get ready to:
  • Launch a public Amazon Linux 2023 EC2 instance with restricted SSH access.
  • Connect to the instance from Windows PowerShell using its PEM key.
  • Install the web server, PHP runtime, dependency manager, and database server.
Create the network boundary and launch EC2

A security group controls which incoming connections can reach your instance. SSH stays limited to your current public IP address. HTTP remains public so your browser can reach Apache.

  • Return to the AWS Management Console from your account access check.
  • Choose one AWS Region from the Region selector in the top navigation bar.
  • Record the selected Region here: your chosen AWS Region.
  • Enter EC2 in the console search bar.
  • Select EC2 from the search results.

Why stay in one Region?

AWS resources belong to a specific Region. Keeping the instance in your chosen AWS Region makes it easier to find now. API Gateway will use the same Region later.

  • Select Security Groups under Network & Security in the left navigation pane.
  • Click Create security group.
  • Enter laravel-login-api-sg in the Security group name field.
  • Enter Security group for the Laravel login API in the Description field.
  • Keep the default VPC selected in the VPC field.
  • Click Add rule under Inbound rules.
  • Select SSH as the rule type.
  • Select My IP as the source.

What does the SSH rule allow?

The SSH rule allows TCP traffic on port 22 from your current public IPv4 address. Other public IPv4 addresses cannot use this rule to start an SSH connection.

  • Click Add rule again.
  • Select HTTP as the second rule type.
  • Select Anywhere-IPv4 as the source.
  • Click Create security group.

You will see laravel-login-api-sg in the security group list. Its inbound rules permit TCP 22 from your IP address. They also permit TCP 80 from the internet.

  • Select EC2 Dashboard in the left navigation pane.
  • Click Launch instance.
  • Enter laravel-login-api in the Name field.
  • Choose Amazon Linux under Application and OS Images.
  • Select the latest standard Amazon Linux 2023 AMI.
  • Select the smallest micro-sized instance currently marked Free tier eligible for your account.

Check the Cost Before Launching

Launching starts an EC2 instance with an EBS root volume. Free Tier eligibility varies by account. A running instance or its volume can incur usage-based charges.

Keep the instance only for this disposable learning environment. The cleanup section shows how to stop or delete the resources.

  • Click Create new key pair under Key pair (login).
  • Enter laravel-login-api-key in the Key pair name field.
  • Select RSA as the Key pair type.
  • Select .pem as the Private key file format.
  • Click Create key pair.

Keep the PEM File Safe

The browser downloads the PEM private key once. AWS does not keep a recoverable copy of this private key.

OpenSSH uses the file to prove that your PowerShell connection belongs to the key pair attached to the instance. Keep the file private.

  • Record the downloaded PEM path here: C:\Users\YourName\Downloads\laravel-login-api-key.pem.
  • Click Edit under Network settings.
  • Select Select existing security group under Firewall (security groups).
  • Choose laravel-login-api-sg under Common security groups.
  • Confirm Auto-assign public IP shows Enable.
  • Click Launch instance in the Summary panel.

Provisioning can take a few minutes. The instance is ready for the next check after both status checks pass.

  • Click View all instances.
  • Wait until the Instance state column shows Running.
  • Wait until the Status check column shows both checks passing.
  • Select the laravel-login-api instance.
  • Record the Public IPv4 address here: your EC2 public IPv4 address.
  • Record the Public IPv4 DNS here: your EC2 public DNS name.
Connect from Windows PowerShell

The instance is running in AWS. Your PEM file now authenticates a remote shell from your Windows computer.

  • Click Connect above the instance details.
  • Select the SSH client tab.
  • Copy the command shown under the SSH example.
  • Compare the copied command with this structure:
ssh -i "[[PEM_PATH="C:\Users\YourName\Downloads\laravel-login-api-key.pem"]]" ec2-user@[[EC2_PUBLIC_DNS="your EC2 public DNS name"]]

What Does This Command Use?

  • The -i option points OpenSSH to your downloaded PEM file.
  • The ec2-user account is the standard login user for this Amazon Linux 2023 instance.
  • The public DNS name directs the SSH connection to your EC2 instance.
  • Press the Windows key to open the Windows search bar.
  • Type Windows PowerShell into the search bar.
  • Press Enter to open Windows PowerShell.
  • Paste the copied SSH command into PowerShell.
  • Press Enter to start the connection.
  • Approve the host fingerprint prompt if OpenSSH displays it on this first connection.

You will see an Amazon Linux shell prompt for ec2-user. Your commands now run on the EC2 instance.

SSH Connection Not Opening?

Confirm the instance still shows both status checks passing. Confirm the security group's SSH source matches your current public IP address.

If OpenSSH rejects the PEM file permissions, use the file's Windows security settings to remove access for other users. Keep read access for your Windows account.

Need a hand? Help me troubleshoot my EC2 SSH connection from Windows PowerShell.

Install and verify the server stack

Laravel needs a web server to accept HTTP requests. It also needs a compatible PHP runtime. Composer manages its PHP dependencies. MariaDB persists its application data.

The package update may run quietly for a few minutes while Amazon Linux downloads security fixes. That delay is normal on a fresh instance.

  • Update the Amazon Linux 2023 packages from the SSH session by running:
sudo dnf upgrade -y

What Does This Command Do?

The dnf package manager downloads available updates for installed packages. The -y option accepts the update confirmation automatically.

The terminal returns to the shell prompt after the update finishes. Your instance now has the repository updates needed for the versioned packages below.

Package Update Failed?

Confirm the SSH session is still connected. A temporary repository or network failure can usually be resolved by rerunning the update command.

Need help with the output? Help me diagnose my Amazon Linux package update.

  • Install Apache, PHP 8.5, Composer 2.10.2, and MariaDB 10.5 by running:
sudo dnf install -y httpd
sudo dnf install -y php8.5 php8.5-cli php8.5-common php8.5-mbstring php8.5-modphp php8.5-mysqlnd php8.5-pdo php8.5-xml php8.5-zip
sudo dnf install -y composer
sudo dnf install mariadb105-server

What Gets Installed?

  • The httpd package provides Apache HTTP Server.
  • The PHP packages provide the 8.5 runtime. They also provide the database, XML, archive, and text-processing extensions Laravel needs.
  • The composer package provides Composer for Laravel dependency installation.
  • The mariadb105-server package provides the MariaDB 10.5 database server.

Each installation finishes by returning you to the shell prompt. The package summary should show the requested packages as installed.

Seeing a Missing Package?

Confirm the earlier package upgrade completed. PHP 8.5 requires an Amazon Linux 2023 repository snapshot that includes the versioned package line.

Need help with a package name? Help me troubleshoot the Amazon Linux server stack installation.

  • Start Apache and MariaDB at boot by running:
sudo systemctl start httpd
sudo systemctl enable httpd
sudo systemctl start mariadb
sudo systemctl enable mariadb

What Do These Service Commands Do?

The start commands run Apache and MariaDB now. The enable commands configure both services to start after an instance boot.

Apache listens for the HTTP traffic permitted by your security group. MariaDB stays inside the EC2 instance because the security group does not expose its database port.

Before you test the public address, predict what the browser should show when port 80 reaches a running Apache server.

  • Open Apache's public HTTP address by entering this URL in a new browser tab:
http://[[EC2_PUBLIC_DNS="your EC2 public DNS name"]]

What Does This URL Test?

The public DNS name resolves to your EC2 instance. The HTTP scheme sends the request to TCP port 80.

A successful page proves that the public address, security group, and Apache service work together.

You will see Apache's It works! page. That is the first public response from your server.

Apache Page Not Loading?

Confirm the URL begins with HTTP. Confirm the security group has an HTTP rule for TCP port 80 from Anywhere-IPv4.

Return to the SSH session if the browser times out. Check whether Apache started successfully before retrying the page.

Still stuck? Help me troubleshoot why my EC2 Apache page is unreachable.

Before you run the final checks, predict which versions should appear for PHP, Composer, and MariaDB. Also predict the enabled state for Apache.

  • Check the runtime versions and service states from the SSH session by running:
php --version
composer --version
mysql --version
sudo systemctl is-enabled httpd
sudo systemctl status mariadb

What Do These Checks Prove?

  • The PHP output confirms that the command-line runtime is PHP 8.5.
  • The Composer output confirms version 2.10.2.
  • The database client output identifies MariaDB 10.5.
  • The service checks confirm that Apache is enabled. They also show MariaDB running.
  • Press q to leave the MariaDB status view.

You should see PHP 8.5, Composer 2.10.2, and MariaDB 10.5 in the output. Apache should report an enabled state. MariaDB should report an active state.

Version or Service Check Failed?

A missing version command points to the matching package installation. A stopped service points to the matching start command.

Need help reading the checks? Help me diagnose my EC2 server stack verification.

Your EC2 host is reachable. Its Laravel runtime stack is ready. Next, you will replace the generic Apache page with the Laravel foundation.

Deploy the Laravel Foundation

Your Amazon Linux 2023 server already answers public HTTP requests with Apache HTTP Server's test page. That proves the host is reachable.

The generic page cannot route API requests or persist users. This step connects Laravel to MariaDB. It also gives Apache a safe public document root.

In this step, get ready to:
  • Create a localhost-only database account for the Laravel application.
  • Deploy Laravel 13.32.0 with Laravel Sanctum 4.3.3.
  • Serve Laravel from its public directory through Apache.
Create the Laravel database

Laravel needs a database for users and authentication tokens. A service account scoped to localhost keeps database access on the EC2 instance.

Why MariaDB for this API?

MariaDB has a documented installation path on Amazon Linux 2023. Laravel also supports its MySQL-compatible connection directly.

This keeps the authentication data in a database service designed for persistent server workloads.

The database password is sensitive. Use a disposable password for this learning environment.

  • Switch back to the SSH session from the previous step.
  • Start the local MariaDB administrative client by running this command:
sudo mariadb

What does this command do?

The command uses the instance's administrative operating system identity to connect through MariaDB's local Unix socket. It does not place a database root password in your command history.

  • Choose a disposable database password that you do not use anywhere else.
  • At the MariaDB prompt, replace your-database-password-here with that password in the statements below.
  • Create the database account by running these statements:
CREATE DATABASE login_api;
CREATE USER 'login_api'@'localhost' IDENTIFIED BY 'your-database-password-here';
GRANT ALL PRIVILEGES ON login_api.* TO 'login_api'@'localhost';
SHOW DATABASES LIKE 'login_api';
SELECT User, Host FROM mysql.user WHERE User = 'login_api';
EXIT;

What do these statements create?

  • The login_api database stores Laravel's application tables.
  • The 'login_api'@'localhost' account accepts local socket connections from this server.
  • The grant limits the service account to the login_api database.
  • The final queries confirm the database name plus the account host without printing the password.

You should see login_api in the database result. The account result should pair login_api with localhost.

Database setup did not complete?

Make sure the MariaDB service from the previous step is still running. Check that every SQL statement ends with a terminator.

If the database or user already exists from an earlier attempt, remove that partial resource before repeating the statements.

Still stuck? Help me troubleshoot my localhost MariaDB user and database creation.

Install Laravel and API support

The Laravel skeleton provides the application structure plus a generated application key. Composer resolves its framework dependencies inside /var/www/login-api.

  • Create the /var/www/login-api directory with ownership for ec2-user by running these commands:
sudo mkdir -p /var/www/login-api
sudo chown ec2-user:ec2-user /var/www/login-api
composer create-project laravel/laravel /var/www/login-api v13.10.1

What does this setup create?

The first two commands prepare the application directory for your SSH user. Composer then installs the laravel/laravel skeleton at version v13.10.1.

The skeleton creates the Laravel directories plus an initial .env file. Its setup script also generates an application key.

  • Confirm Composer finishes without reporting an unresolved dependency.
  • Confirm the output shows Laravel's post-install commands completing.

Good progress. The generic Apache host now contains a complete Laravel application foundation.

Composer could not create the project?

Check that the target directory is empty. Confirm that ec2-user owns it.

A package download failure can also come from a temporary network problem. Repeat the Composer command after confirming the instance can reach the internet.

Need a hand? Help me diagnose my Laravel Composer installation.

  • Resolve Laravel Framework 13.32.0 inside the new application by running these commands:
cd /var/www/login-api
composer require laravel/framework:v13.32.0

Why pin the framework version?

The skeleton accepts compatible Laravel releases through its Composer constraint. This command records framework version v13.32.0 in the resolved dependency set.

  • Return to the EC2 instance details page in the AWS Management Console from earlier.
  • Copy the instance's public DNS name.
  • Record the copied value here: your-ec2-public-dns.
  • Create the production .env structure by running this command in the SSH session:
sudo tee /var/www/login-api/.env > /dev/null <<'EOF'
APP_NAME="Login API"
APP_ENV=production
APP_KEY=your-generated-app-key-here
APP_DEBUG=false
APP_URL=http://your-ec2-public-dns

LOG_CHANNEL=stack
LOG_LEVEL=warning

DB_CONNECTION=mysql
DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=login_api
DB_USERNAME=login_api
DB_PASSWORD=your-database-password-here

SESSION_DRIVER=file
CACHE_STORE=file
QUEUE_CONNECTION=sync
MAIL_MAILER=log
EOF

What does this configuration control?

  • The application settings identify this deployment as production.
  • The debug setting prevents detailed Laravel errors from being shown publicly.
  • The database settings point Laravel to the local login_api service account.
  • The session plus cache settings keep this small API deployment local to the instance.

The next command asks for your database password without echoing it to the terminal. It also keeps the password out of the command itself.

  • Insert the database password into .env through a hidden prompt by running these commands:
cd /var/www/login-api
read -s -p "Database password: " DB_PASSWORD
echo
DB_PASSWORD="$DB_PASSWORD" php -r '$p=".env";$c=file_get_contents($p);$c=str_replace("your-database-password-here",getenv("DB_PASSWORD"),$c);file_put_contents($p,$c);'
unset DB_PASSWORD

How is the password handled?

The shell reads the password without displaying it. PHP replaces the temporary placeholder inside .env.

The final command removes the temporary shell variable. Do not print or screenshot the resulting file.

  • Insert the saved EC2 public DNS name into .env by running this command:
EC2_PUBLIC_DNS="[[EC2_PUBLIC_DNS="your-ec2-public-dns"]]" php -r '$p=".env";$c=file_get_contents($p);$c=str_replace("your-ec2-public-dns",getenv("EC2_PUBLIC_DNS"),$c);file_put_contents($p,$c);'

What changes in the application URL?

The command replaces the template hostname with your instance's public DNS name. Laravel can now report the public base URL for this deployment.

  • Generate the final application key by running these commands:
cd /var/www/login-api
php artisan key:generate --ansi

What is the application key?

Laravel uses APP_KEY for application-level encryption. The command replaces the temporary key placeholder with a generated value.

  • Confirm the key-generation command reports success.
  • Keep the real APP_KEY plus database password out of screenshots.

Application key generation failed?

Confirm your current directory is /var/www/login-api. Check that the .env file exists there.

If Laravel reports a database-related problem during later commands, recheck the database name plus username. Re-enter the hidden password if those values are correct.

Still blocked? Help me diagnose my Laravel environment configuration without exposing secrets.

✔️ Awesome, I've got everything!

Great. Your .env now contains the production structure with your generated key plus private database password.

ⓧ I'd like to double check the full code

Compare the structure below with /var/www/login-api/.env. Keep your generated key plus private values in the real file.

APP_NAME="Login API"
APP_ENV=production
APP_KEY=your-generated-app-key-here
APP_DEBUG=false
APP_URL=http://your-ec2-public-dns

LOG_CHANNEL=stack
LOG_LEVEL=warning

DB_CONNECTION=mysql
DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=login_api
DB_USERNAME=login_api
DB_PASSWORD=your-database-password-here

SESSION_DRIVER=file
CACHE_STORE=file
QUEUE_CONNECTION=sync
MAIL_MAILER=log

Laravel's API installer publishes routes/api.php plus the Sanctum token migration. The migration creates the database tables that later store users and API tokens.

  • Prepare for the API installer to offer a migration prompt.
  • Decline that prompt so you can pin Sanctum before running all migrations together.
  • Install the API support with the required Sanctum version by running these commands:
cd /var/www/login-api
php artisan install:api
composer require laravel/sanctum:v4.3.3
php artisan migrate --force

What does the API installation add?

  • The API installer creates routes/api.php plus registers API routing in Laravel's bootstrap configuration.
  • Sanctum v4.3.3 provides the bearer-token authentication layer used later.
  • The migration command creates the application tables plus Sanctum's personal-access-token table in MariaDB.
  • Inspect the installed framework versions plus migration state by running these commands:
composer show laravel/framework
composer show laravel/sanctum
php artisan migrate:status

What should the checks confirm?

The package details should identify Laravel Framework v13.32.0 plus Laravel Sanctum v4.3.3.

The migration list should show the application migrations plus the personal-access-token migration as completed.

API installation or migrations failed?

A database connection failure usually means the password in .env does not match the password assigned to the local MariaDB account.

A missing API route file usually means the API installer did not finish. Repeat that installer after resolving the reported Composer problem.

Need help reading the output? Help me troubleshoot Laravel Sanctum installation or MariaDB migrations.

Serve Laravel through Apache

Laravel needs Apache to serve only its public directory. The web server also needs write access to storage plus bootstrap/cache.

  • Set read access across the application with write access limited to Laravel's runtime directories by running these commands:
sudo chown -R ec2-user:apache /var/www/login-api
sudo find /var/www/login-api -type d -exec chmod 2755 {} \;
sudo find /var/www/login-api -type f -exec chmod 0644 {} \;
sudo chmod 0640 /var/www/login-api/.env
sudo find /var/www/login-api/storage /var/www/login-api/bootstrap/cache -type d -exec chmod 2775 {} \;
sudo find /var/www/login-api/storage /var/www/login-api/bootstrap/cache -type f -exec chmod 0664 {} \;

Why use different permissions?

Apache can read the application files without editing them. The apache group can write only to Laravel's runtime directories.

The private .env file remains readable by the application process. Other users cannot read it.

  • Inspect the protected file plus writable runtime directories by running these commands:
ls -l /var/www/login-api/.env
ls -ld /var/www/login-api/storage /var/www/login-api/bootstrap/cache

What should the permissions show?

The .env file should be readable by its owner plus group. The two runtime directories should include group write access for Apache.

Permissions do not match?

Confirm every path begins with /var/www/login-api. A path typo can change permissions on the wrong directory.

If Laravel later reports that it cannot write a cache or log file, repeat only the permission commands for storage plus bootstrap/cache.

Need help checking the modes? Help me troubleshoot Laravel directory permissions for Apache.

Apache loads virtual-host fragments from its configuration directory. This fragment points port 80 at Laravel's public entry point.

  • Create /etc/httpd/conf.d/login-api.conf with the required virtual host by running this command:
sudo tee /etc/httpd/conf.d/login-api.conf > /dev/null <<'EOF'
<VirtualHost *:80>
    DocumentRoot "/var/www/login-api/public"

    <Directory "/var/www/login-api/public">
        AllowOverride All
        Require all granted
    </Directory>
</VirtualHost>
EOF

What does this virtual host do?

  • The document root exposes only /var/www/login-api/public to HTTP requests.
  • The directory block allows Laravel's rewrite rules to route requests through its public entry point.
  • The access rule permits public requests already allowed by the EC2 security group.

✔️ Awesome, I've got everything!

Your Apache virtual host now points at Laravel's public directory.

ⓧ I'd like to double check the full code

Compare your login-api.conf file with this complete reference.

<VirtualHost *:80>
    DocumentRoot "/var/www/login-api/public"

    <Directory "/var/www/login-api/public">
        AllowOverride All
        Require all granted
    </Directory>
</VirtualHost>
  • Apply the new virtual-host configuration by restarting Apache:
sudo systemctl restart httpd

What does the restart apply?

Apache reloads the virtual-host fragment when the service starts again. Requests on port 80 now enter Laravel through its public directory.

  • Press the Windows key to open the search bar.
  • Type Windows PowerShell into the search bar.
  • Press Enter to open a second PowerShell window.

Before you run the health check, what status code do you expect from a Laravel application that boots successfully?

  • Request Laravel's public health route through the EC2 address by running these commands:
$Response = Invoke-WebRequest -Uri "http://[[EC2_PUBLIC_DNS="your-ec2-public-dns"]]/up"
$Response.StatusCode

What does the health check prove?

The request travels through the public EC2 security-group rule to Apache. Apache then serves Laravel from /var/www/login-api/public.

Laravel returns 200 only when the application boots without an exception.

You should see 200 in PowerShell. That is the hard part done: your public server now runs Laravel instead of the generic Apache page.

Health route does not return 200?

If Apache did not restart, compare login-api.conf with the full reference. Check every closing tag plus quoted path.

If the server returns an application error, recheck the private database password in .env. Confirm the Laravel runtime directories remain writable by the apache group.

Still not seeing 200? Help me troubleshoot my Laravel health route through Apache.

Your database-backed Laravel foundation is live on EC2. Next, you will add a protected API route and experience its authentication barrier.

Protect the Profile Route

Your Laravel health route proves that the application boots on your public EC2 server. The health check does not protect user information.

A profile endpoint exposes user data. It needs an authentication boundary.

This step uses Laravel Sanctum to require a bearer token before profile fields are returned. You will probe that boundary without credentials.

In this step, get ready to:
  • Give the user model Laravel Sanctum token capabilities.
  • Create a protected profile route that returns safe user fields.
  • Probe the guard from Windows PowerShell without an Authorization header.
Enable API tokens on the user model

Sanctum adds token functionality to an application through the user model. The HasApiTokens trait gives the model the methods needed to create tokens later.

  • Use the terminal editor in your SSH session from earlier to replace the contents of /var/www/login-api/app/Models/User.php with this code:
<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens, Notifiable;
}

What does this model change do?

  • The HasApiTokens import makes Sanctum's token trait available to the model.
  • The use HasApiTokens, Notifiable; line adds token support while preserving Laravel's notification support.
  • The Authenticatable base class keeps the model connected to Laravel's authentication system.
  • Save app/Models/User.php.
  • Return to the browser tab showing the /up route from earlier.
  • Refresh the health route.

Your /up route should still return HTTP 200.

Good progress. Your user model now supports API tokens while the Laravel health check still passes.

Did the health route stop working?

Compare app/Models/User.php with the reference below. Check the namespace spelling plus each imported class.

Still stuck? Help me find the mistake in my Laravel User model after adding HasApiTokens.

✔️ Awesome, I've got everything!

Your saved user model has Sanctum token capabilities. Its health check still passes.

ⓧ I'd like to double check the full code

  • Compare your entire app/Models/User.php file with this reference:
<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens, Notifiable;
}

What should match?

  • The file should contain the Laravel\Sanctum\HasApiTokens import.
  • The class should use HasApiTokens, Notifiable.
Define the protected profile route

The auth:sanctum guard is middleware that authenticates a request before the route closure runs. The closure can then read the authenticated user safely.

The response returns only the user's ID, name, and email. Password data stays outside the response.

  • Use the terminal editor in your SSH session to replace the contents of /var/www/login-api/routes/api.php with this code:
<?php

use Illuminate\Http\Request;

Route::get('/me', function (Request $request) {
    return [
        'id' => $request->user()->id,
        'name' => $request->user()->name,
        'email' => $request->user()->email,
    ];
})->middleware('auth:sanctum');

How is the profile protected?

  • The Request import gives the closure access to the current HTTP request.
  • The GET /api/me route returns the authenticated user's safe profile fields.
  • The auth:sanctum middleware requires a token that Sanctum recognizes.
  • Save routes/api.php.
Prove the authentication barrier

A protected route is only useful when unauthenticated requests cannot reach its data. You will call the profile endpoint without an Authorization header.

  • Leave the SSH window from earlier connected.
  • Press the Windows key to open search.
  • Type Windows PowerShell into the search field.
  • Press Enter to open a second PowerShell window.
  • Record the EC2 public DNS from the /up address you tested earlier as <EC2-public-DNS>.

Before you send the request, do you think the profile route will return user data or reject the call?

  • Test the profile route without credentials by running this command:
Invoke-RestMethod -Uri "http://[[EC2_PUBLIC_DNS="<EC2-public-DNS>"]]/api/me" -Method GET -Headers @{ Accept = "application/json" }

The rejection is intentional

Windows PowerShell reports 401 Unauthorized. This is the designed shortfall for this step.

The request reaches Laravel without an Authorization header. The auth:sanctum middleware blocks it before the closure returns any profile fields.

Did you get a different result?

If the request reaches another page, confirm that the URL ends with /api/me.

If profile fields are returned, compare the final line of routes/api.php with the reference below. The route must use auth:sanctum.

Need help? Help me troubleshoot why my Laravel profile route does not return 401 Unauthorized without a token.

✔️ Awesome, I've got everything!

Your profile route now rejects unauthenticated requests. The authentication boundary is working.

ⓧ I'd like to double check the full code

  • Compare your entire routes/api.php file with this reference:
<?php

use Illuminate\Http\Request;

Route::get('/me', function (Request $request) {
    return [
        'id' => $request->user()->id,
        'name' => $request->user()->name,
        'email' => $request->user()->email,
    ];
})->middleware('auth:sanctum');

What should match?

  • The route should use GET /api/me.
  • The response should contain only id, name, and email.
  • The final line should attach the auth:sanctum middleware.

Your profile endpoint now has a working authentication barrier. Next, you will exchange disposable demo credentials for a token that can cross it.

Issue a Bearer Token

Your Laravel API on Amazon EC2 now protects /api/me. The earlier 401 Unauthorized response proved that anonymous requests cannot reach the demo profile.

The protected route needs a secure way to recognize an approved client. In this step, Laravel Sanctum exchanges disposable credentials for a bearer token that the client presents with later requests.

In this step, get ready to:
  • Create one disposable demo user in MariaDB.
  • Add a login route that issues a Sanctum token for valid credentials.
  • Use PowerShell to exchange the demo credentials for a token.
Create a disposable demo user

The login route needs one database-backed identity to verify. Laravel Tinker gives you an interactive shell for creating that user through the existing Laravel model.

Keep these credentials disposable

Sending a password through the public HTTP origin is the risky moment in this step. Use a password that has never protected another account.

Use the fictional email address shown below. Never enter personal data or a production secret in this learning environment.

  • Return to the Laravel application directory and enter Tinker by running these commands:
cd /var/www/login-api
php artisan tinker

What does Tinker do?

Tinker boots the Laravel application inside an interactive prompt. Your model uses the production database connection from .env.

  • Replace your-disposable-demo-password-here with a password created only for this project.
  • Create the demo user by pasting the following lines into the Tinker prompt:
// Create a disposable user for this HTTP-only learning environment.
$user = new App\Models\User;
$user->name = 'Demo User';
$user->email = 'demo@example.com';

// Hash the demo password before storing it.
$user->password = Illuminate\Support\Facades\Hash::make('your-disposable-demo-password-here');
$user->save();

// Display only fields that are safe to confirm.
$user->only(['id', 'name', 'email']);

What does this code do?

  • The User model represents the new database record.
  • Hash::make converts the disposable password into a stored password hash.
  • save() writes the demo user to the users table.
  • only() displays the safe profile fields without revealing the password hash.

You should see the new user's id, name, and email fields. That disposable identity is now ready for the login exchange.

  • Press Ctrl+D to leave Tinker.

Demo user not saved?

Check that the email address contains no typing errors. Confirm that every PHP statement ends with a semicolon.

If Tinker reports a database problem, confirm that Laravel still uses the login_api connection from .env.

Still stuck? Help me troubleshoot why my Laravel user did not save in Tinker.

Add the login endpoint

The login endpoint checks the submitted credentials against the demo user. A successful match creates a Sanctum token that represents the client on later requests.

Editing through Vim can feel fiddly because command mode and insert mode use different keystrokes. You will make each change separately so syntax mistakes stay easy to find.

  • Open /var/www/login-api/routes/api.php in Vim by running this command:
vi /var/www/login-api/routes/api.php

How does Vim editing work?

Vim opens in command mode. Pressing i switches to insert mode so you can change the file.

  • Press i to enter insert mode.
  • Find the existing use Illuminate\Http\Request; import.
  • Replace that import with the following group:
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;

Why are these imports needed?

  • User finds the demo account by email address.
  • Request provides the submitted login fields.
  • Hash compares the submitted password with the stored hash.
  • ValidationException returns a validation response when the credentials do not match.
  • Press Esc to return to command mode.
  • Type :wq to save the import change.
  • Press Enter to return to the shell.
  • Check the updated file for PHP syntax errors by running this command:
php -l /var/www/login-api/routes/api.php

What does the syntax check prove?

The -l option checks PHP syntax without executing the route file. This catches missing semicolons and malformed imports before an API request reaches Apache.

You should see No syntax errors detected in /var/www/login-api/routes/api.php.

Seeing a syntax problem?

Compare the namespace separators in all four imports. Each import must end with one semicolon.

Need another pair of eyes? Help me find the syntax problem in my Laravel import block.

  • Reopen /var/www/login-api/routes/api.php by running this command:
vi /var/www/login-api/routes/api.php

Return to insert mode

Vim starts in command mode each time it opens. Press i before pasting the route.

  • Press i to enter insert mode.
  • Place the cursor above the existing Route::get('/me' route.
  • Insert the login route by pasting this code:
Route::post('/login', function (Request $request) {
    $credentials = $request->validate([
        'email' => ['required', 'email'],
        'password' => ['required'],
        'device_name' => ['required'],
    ]);

    $user = User::where('email', $credentials['email'])->first();

    if (! $user || ! Hash::check($credentials['password'], $user->password)) {
        throw ValidationException::withMessages([
            'email' => ['The provided credentials are incorrect.'],
        ]);
    }

    return [
        'token' => $user->createToken($credentials['device_name'])->plainTextToken,
    ];
});

How does the login route work?

  • $request->validate() requires an email address, password, and device name.
  • User::where() retrieves the account with the submitted email address.
  • Hash::check() verifies the submitted password against the stored hash.
  • createToken() creates a Sanctum access token for the named client.
  • plainTextToken returns the token value once so the client can store it.
  • Press Esc to return to command mode.
  • Type :wq to save the login route.
  • Press Enter to return to the shell.
  • Check the completed route file for PHP syntax errors by running this command:
php -l /var/www/login-api/routes/api.php

What does this check confirm?

This syntax check covers the imports, the new login route, and the existing protected profile route. A clean result means PHP can parse the complete file.

You should see No syntax errors detected in /var/www/login-api/routes/api.php again.

Route file still failing the check?

Check the closing brackets around the validation rules. Confirm that the login closure ends before the existing profile route begins.

Still seeing a problem? Help me compare my login route with the expected Laravel route structure.

✔️ Awesome, I've got everything!

Your route file now contains the login exchange and the existing protected profile endpoint.

ⓧ I'd like to double check the full code

<?php

use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;

Route::post('/login', function (Request $request) {
    $credentials = $request->validate([
        'email' => ['required', 'email'],
        'password' => ['required'],
        'device_name' => ['required'],
    ]);

    $user = User::where('email', $credentials['email'])->first();

    if (! $user || ! Hash::check($credentials['password'], $user->password)) {
        throw ValidationException::withMessages([
            'email' => ['The provided credentials are incorrect.'],
        ]);
    }

    return [
        'token' => $user->createToken($credentials['device_name'])->plainTextToken,
    ];
});

Route::get('/me', function (Request $request) {
    return [
        'id' => $request->user()->id,
        'name' => $request->user()->name,
        'email' => $request->user()->email,
    ];
})->middleware('auth:sanctum');
Log in and call the protected route

The login request turns the disposable credentials into a token. PowerShell keeps that token in memory so later requests can send it without printing the value.

Protect the disposable token

The direct EC2 endpoint sends this request over public HTTP. Keep using only the disposable password from Tinker.

Avoid printing or sharing the returned token. Anyone holding that token can use it until it is revoked or the environment is deleted.

  • Switch back to the local PowerShell window from the unauthenticated test.
  • Replace your-ec2-public-dns with the public DNS name of your EC2 instance.
  • Replace your-disposable-demo-password-here with the same disposable password used in Tinker.
  • Exchange the disposable credentials for a token by running this PowerShell code:
# Point this client at the public EC2 origin.
$baseUrl = "http://[[EC2_PUBLIC_DNS=\"your-ec2-public-dns\"]]"

# Build the disposable login request as JSON.
$loginBody = @{
    email = "demo@example.com"
    password = "your-disposable-demo-password-here"
    device_name = "powershell-client"
} | ConvertTo-Json

# Exchange the credentials without printing the returned token.
$loginResponse = Invoke-RestMethod -Uri "$baseUrl/api/login" -Method Post -ContentType "application/json" -Body $loginBody -Headers @{ Accept = "application/json" }
$token = $loginResponse.token
$token.Length -gt 0

What does this PowerShell code do?

  • ConvertTo-Json serializes the three login fields into the request body.
  • Invoke-RestMethod sends the JSON body to /api/login.
  • $loginResponse.token extracts the returned token into $token.
  • $token.Length -gt 0 confirms that PowerShell saved a non-empty token without revealing it.

You should see a positive Boolean result. That confirms the credential exchange returned a token and saved it in $token.

Login request rejected?

Confirm that the email remains demo@example.com. Re-enter the same disposable password used when you created the user.

Check that the base URL begins with http:// and contains the current EC2 public DNS name.

Need help without exposing your credentials? Help me troubleshoot why my Laravel login request is being rejected.

Before you run the protected request, do you expect the saved token to satisfy the same guard that returned 401 Unauthorized earlier?

  • Call the protected profile route with the bearer token by running this PowerShell code:
# Send the saved token in the authorization header.
$headers = @{
    Accept = "application/json"
    Authorization = "Bearer $token"
}

# Request only the protected profile fields.
Invoke-RestMethod -Uri "$baseUrl/api/me" -Method Get -Headers $headers

How does the bearer header work?

The Authorization header presents the saved token with the Bearer scheme. Sanctum resolves that token to the demo user before the route runs.

The route returns only the user's id, name, and email fields.

You should see the demo user's numeric ID, the name Demo User, and the email address demo@example.com in PowerShell.

You cleared the authentication barrier. The direct EC2 API can now exchange disposable credentials for a token and use that token to retrieve the protected profile.

Still receiving an unauthorized response?

Run the login request again in the same PowerShell window. Confirm that $token receives the latest returned token.

Check that the header value starts with Bearer followed by one space. Keep the token itself private.

Still blocked? Help me troubleshoot why Laravel Sanctum rejects my saved bearer token.

Your token-authenticated API now works through the direct EC2 origin. Next, you will place an HTTPS API Gateway endpoint in front of it.

Proxy the API Through Gateway

Your Laravel API now issues a Laravel Sanctum bearer token for disposable credentials.

The direct Amazon EC2 endpoint returns the protected demo profile when that token is present.

Direct EC2 calls leave API Gateway out of the request path.

In this step, an Amazon API Gateway HTTP API provides a public HTTPS endpoint. Its catch-all route forwards each request to the existing EC2 origin.

In this step, get ready to:
  • Create an HTTP endpoint integration for the public EC2 origin.
  • Publish the integration through a catch-all route with an auto-deploying stage.
  • Prove the protected profile flow through the Gateway HTTPS endpoint.
Create the HTTP endpoint integration

An HTTP proxy integration passes an incoming request to a publicly reachable backend. It returns the backend response to the caller.

Why Use an HTTP API?

An HTTP API fits this project because Gateway only needs to pass requests through to one public origin.

A REST API includes broader controls that this learning proxy does not need.

  • Switch back to the Windows PowerShell window from earlier.
  • Recall the direct EC2 origin by finding the successful login request in your PowerShell history.
  • Store the origin without /api/login here: http://your-ec2-public-dns.

This origin is the backend address that Gateway uses for every proxied path.

  • Return to the AWS Management Console from earlier.
  • Enter API Gateway in the top search bar.
  • Select Amazon API Gateway from the results.
  • Click Create API.
  • Click Build under HTTP API.
  • Click Add integration.
  • Choose the HTTP endpoint integration type.
  • Paste http://your-ec2-public-dns into the integration endpoint field.

The integration now targets the same public EC2 origin that served your direct API requests.

Is the Origin Rejected?

  • Confirm the origin starts with http://.
  • Remove /api/login or any other route from the origin.
  • Confirm the public EC2 address still opens the Laravel /up route.

Still stuck? Help me check why API Gateway will not accept my EC2 HTTP origin.

Create the default route and stage

The $default route catches requests that do not match a more specific route. The $default stage deploys changes automatically.

  • Enter laravel-login-api in the Name field.
  • Click Review and create.
  • Confirm the summary uses http://your-ec2-public-dns as the integration endpoint.

Before You Create the API

This creates a usage-billed AWS resource. The requests in this exercise are small, but account eligibility determines whether charges apply.

API Gateway has no minimum fee or upfront commitment. HTTP API billing is based on calls received plus data transferred out.

  • Click Create.

Good progress. laravel-login-api is now a live HTTP API connected to your EC2 origin.

  • Open the API's route configuration from its details page.
  • Confirm $default points to the HTTP endpoint integration.
  • Open the API's stage configuration from its details page.
  • Confirm the $default stage deploys changes automatically.
  • Copy the invoke URL from the API details.
  • Store the invoke URL here: https://<api-id>.execute-api.<region>.amazonaws.com.

Know the Security Boundary

The invoke URL protects the client-to-Gateway connection with HTTPS. Your requests from PowerShell are encrypted on that leg.

Gateway still connects to the public EC2 origin over HTTP. The EC2 origin also remains directly reachable.

This remains a disposable learning architecture. Continue using demo credentials only.

Before loading the health route, do you expect Gateway to return the same Laravel health result as EC2?

  • Open a new browser tab.
  • Paste https://<api-id>.execute-api.<region>.amazonaws.com into the address bar.
  • Append /up to the URL.
  • Load the health route by pressing Enter.

You should see the successful Laravel health response through the Gateway URL. This proves the catch-all route forwarded /up to EC2.

Does the Health Check Fail?

  • Confirm the API has a $default route connected to the HTTP endpoint integration.
  • Confirm the $default stage deploys automatically.
  • Confirm the direct EC2 /up address still responds before testing Gateway again.

Need a hand? Help me trace why my API Gateway health request is not reaching Laravel on EC2.

Test the proxied authentication flow

Gateway changes the public entry point for your requests. Laravel keeps the same login route plus the same Sanctum protection.

  • Switch back to the PowerShell window used for the direct EC2 API calls.
  • Recall the successful login request from earlier using your PowerShell command history.
  • Replace the direct EC2 base URL with https://<api-id>.execute-api.<region>.amazonaws.com.
  • Send the login request by pressing Enter.

PowerShell should receive a new token response through the HTTPS invoke URL.

  • Repeat the token-saving action from the direct login test.

Before sending the protected request, do you expect the demo profile to match the result returned directly from EC2?

  • Recall the successful /api/me request from earlier using your PowerShell command history.
  • Replace only the direct EC2 base URL with https://<api-id>.execute-api.<region>.amazonaws.com.
  • Send the protected request by pressing Enter.

You should see the same demo user's ID, name, and email. The response proves Gateway forwarded the login request plus the bearer-authenticated profile request.

Does the Profile Request Fail?

  • Confirm the login request uses the Gateway invoke URL with /api/login.
  • Confirm the protected request uses the Gateway invoke URL with /api/me.
  • Confirm the bearer header contains the new token returned through Gateway.

Still blocked? Help me troubleshoot my Laravel Sanctum requests through API Gateway.

Strong finish. The HTTPS Gateway endpoint now issues a token through the EC2-backed Laravel API. The same endpoint returns the protected demo profile.

Secret mission

Revoke the Current Token

Extend your login API with a protected logout route that revokes only the bearer token used for the request. Then prove the same token receives 401 Unauthorized through API Gateway.

Clean Up Your Resources

Your Amazon EC2 instance can incur usage-based charges while it runs. Its Amazon EBS root volume can still incur storage charges while the instance is stopped. Data transfer can also incur charges.

Your Amazon API Gateway HTTP API can incur charges outside applicable credits or allowances. Eligibility varies by account. Pause or Delete is the safer choice when you no longer need the live demo.

Nice Work!

Nice Work!

Excellent work! Your Laravel token login API now runs on Amazon EC2 through a public Amazon API Gateway HTTPS endpoint.

You've learned how to:

  • Provision an Amazon Linux 2023 server behind a restricted EC2 security group. Complete a production Laravel deployment from /var/www/login-api/public through Apache HTTP Server. Confirm the application through its /up health route.
  • Observe the authentication barrier when /api/me rejects a tokenless request with 401 Unauthorized. Store a disposable demo user in MariaDB. Build a Laravel Sanctum login flow at /api/login. Exchange disposable credentials for a bearer token. Use that token to retrieve safe profile fields.
  • Route the API through an Amazon API Gateway HTTP API. Use Windows PowerShell to repeat the login flow against the public HTTPS endpoint. Confirm the proxied /api/me response contains the same safe profile fields.
  • Secret Mission: Add protected current-token revocation through /api/logout. Prove a revoked bearer token returns 401 Unauthorized when reused against /api/me.

Ready to quiz yourself?