Do not use .env to store sensitive credentials in Laravel

If you are working in Laravel, you might have noticed that majority of the developers save sensitive information like database credentials, SMTP settings and API keys in the .env file. This file exists at the root level of your project.

I was also saving my credentials in .env files for a long time until I found some problems in it. So I had to find a better way to save sensitive credentials securely.

Problem with .env file

The problems I found while saving sensitive credentials in .env file in Laravel are:

  1. Caches

The variables you set in .env file will be cached by Laravel. You might change the value of variable but the Laravel will be picking the old cached value.

However, you can refresh cache by running the following commands but it still an extra work:

php artisan config:clear
php artisan cache:clear
php artisan config:cache
  1. Not dynamic

Imagine you are using PayPal or Stripe in your Laravel application and you have saved their API keys in .env file. You have delivered the project but now the client has to change his account again and again. Instead of him messaging you everytime he needs to change his API keys, you can provide him a simple admin panel from where he can set those values dynamically.

  1. Might be exposed

Wrong deployment or 1 bad configuration in .htaccess might lead to exposing your .env file to the public. Hence exposing all your sensitive credentials to everyone.

Database Credentials

Probably the first credential that developers set are the database credentials. Because without them, migrations and seeders won’t run.

I have commented out the database credentials in .env file:

# DB_CONNECTION=sqlite
# DB_HOST=127.0.0.1
# DB_PORT=3306
# DB_DATABASE=laravel
# DB_USERNAME=root
# DB_PASSWORD=

I am setting database credentials directly in config/database.php file.

Change:

'default' => env('DB_CONNECTION', 'sqlite'),

To:

'default' => "mysql",

And change:

'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),

To:

'host' => '127.0.0.1',
'port' => '3306',
'database' => 'database_name',
'username' => 'root',
'password' => '',

Session Lifetime

You can comment out the session lifetime from .env file:

# SESSION_LIFETIME=120

And set this value in config/session.php file:

'lifetime' => 52560000, // 100 years
'expire_on_close' => false,

SMTP Credentials & API Keys

To make SMTP credentials and API keys dynamic, I have created a form at the admin panel:

<form onsubmit="saveSettings()">
    <p>
        <input type="text" class="form-control" name="host" />
    </p>

    <p>
        <input type="text" class="form-control" name="port" />
    </p>

    <p>
        <label>
            SSL
            <input type="radio" name="encryption" value="ssl" />
        </label>

        <label>
            TLS
            <input type="radio" name="encryption" value="tls" />
        </label>
    </p>

    <p>
        <input type="email" name="username" />
    </p>

    <p>
        <input type="password" name="password" />
    </p>

    <button type="submit">Save Settings</button>
</form>

Then in Javascript, I created a function that will be called when this form submits. This function will call an AJAX to save these credentials in the database:

async function saveSettings() {
    event.preventDefault()
    
    const form = event.target
    const formData = new FormData(form)
    form.submit.setAttribute("disabled", "disabled")

    try {
        const response = await axios.post(
            "http://localhost:8000/api/admin/save-settings",
            formData,
            {
                headers: {
                    Authorization: "Bearer " + localStorage.getItem(accessTokenKey)
                }
            }
        )

        if (response.data.status == "success") {
            swal.fire("Save Settings", response.data.message, "success")
        } else {
            swal.fire("Error", response.data.message, "error")
        }
    } catch (exp) {
        swal.fire("Error", exp.message, "error")
    } finally {
        form.submit.removeAttribute("disabled")
    }
}

Then I create a route in routes/api.php file that will handle this AJAX request:

Route::post("/admin/save-settings", [AdminController::class, "save_settings"]);

After that, we need to create a method in AdminController.php that will be called when this route is accessed.

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

use DB;

class AdminController extends Controller
{
    public function save_settings()
    {
        $host = request()->host ?? "";
        $port = request()->port ?? "";
        $encryption = request()->encryption ?? "";
        $username = request()->username ?? "";
        $password = request()->password ?? "";

        $this->set_setting("smtp_host", $host);
        $this->set_setting("smtp_port", $port);
        $this->set_setting("smtp_encryption", $encryption);
        $this->set_setting("smtp_username", $username);
        $this->set_setting("smtp_password", $password);

        return response()->json([
            "status" => "success",
            "message" => "Settings has been saved."
        ]);
    }

    private function set_setting($key, $value)
    {
        $setting = DB::table("settings")
            ->where("key", "=", $key)
            ->first();

        if ($setting == null)
        {
            DB::table("settings")
                ->insertGetId([
                    "key" => $key,
                    "value" => $value,
                    "created_at" => now()->utc(),
                    "updated_at" => now()->utc()
                ]);
        }
        else
        {
            DB::table("settings")
                ->where("id", "=", $setting->id)
                ->update([
                    "value" => $value,
                    "updated_at" => now()->utc()
                ]);
        }
    }
}

This will save credentials in settings table if does not exists. If exists, then it will update its value.

Following is the schema of the settings table:

Schema::create('settings', function (Blueprint $table) {
    $table->id();
    $table->string("key")->nullable();
    $table->longText("value")->nullable();
    $table->timestamps();
});
settings-table
settings-table

Now that you have saved the values in database, I will show you how to fetch the credentials from database.

$settings = DB::table("settings")->get();

$settings_obj = new \stdClass();
foreach ($settings as $setting)
{
    $settings_obj->{$setting->key} = $setting->value;
}

echo $settings_obj->smtp_host;

This will fetch all the credentials from database and convert them into object using key value pairs. So you can access your values just like a normal object.

Hard-coded strings

There might be some scenarios where you do want to save some hard-coded strings. In that case, I have created a new file named “config.php” inside config folder and wrote all my hard-coded variables there:

<?php

// config/config.php

return [
    "app_name" => "adnan-tech.com"
];

And you can access them in the following way:

<p>
    {{ config("config.app_name") }}
</p>

config is a built-in function in Laravel. In “config.app_name”, config is the name of the file I created and app_name is the variable created inside it.