I was recently asked to code review a friend’s first Laravel app, and when I cloned the repository from GitHub I immediately noticed a few big, red flags. Many of these were common mistakes, so I thought I’d take a moment to discuss how we can safely handle credentials and/or sensitive information in our Laravel applications.
Understanding the .env file
Laravel makes use of the phpdotenv library to store environment-specific credentials in a .env
file in the root of the project.
An example .env
file might look something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
APP_NAME=Laravel APP_ENV=local APP_KEY= APP_DEBUG=true APP_URL=http://localhost DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=laravel DB_USERNAME=root DB_PASSWORD= # ... plus a bunch more |
When Laravel is bootstrapping, it checks to see if an .env
file is present and, if so, reads those values into the process environment.
.env
files are extremely useful during development, as each developer will have their own copy with slightly-different configurations, tuned to their own environments.
It’s important that .env files are never checked into source control! Your .env
file should be the single place where all API keys, passwords, etc. are stored — treat it accordingly!
In my friend’s case, he was storing the .env
file in the Git repository, meaning I had access to his credentials as soon as I cloned the repo. Fortunately they were only development credentials, but there were still a few API keys that needed rotating.
Helping other developers with .env.example
Since we’re not committing .env
files into source control, it’s common practice to include a .env.example
file in the repository that contains the necessary information for a new developer on the project to get started.
This example file should not contain any sensitive information, but should have empty environment variables so the new developer knows what might need to be filled in.
If there are non-standard configuration values, it’s also helpful to include explanations and/or instructions to help developers get started:
1 2 3 4 5 6 7 8 9 10 11 |
# Credentials for the Geocoding API. # # This app uses example.com for geocoding customer addresses and # requires a public/private API keypair. # # Visit https://example.com/register, create an account on the free # tier, then click "Generate API key" to get a public + private API # keypair. # EXAMPLECOM_GEOCODING_PUBLIC_KEY= EXAMPLECOM_GEOCODING_PRIVATE_KEY= |
Pro-tip: Automatically copy .env.example to .env
Here’s a nice one-liner that I include in all of my Laravel applications’ composer.json
file:
1 2 3 4 5 6 7 |
{ "scripts": { "post-install-cmd": [ "@php -r \"file_exists('.env') || copy('.env.example', '.env') && system('php artisan key:generate');\"" ], } } |
This adds a post-install-cmd
script that automatically checks for the presence of an .env
file in the local copy of the project. If one doesn’t exist, .env.example
is copied to .env
, then Composer automatically runs php artisan key:generate
, which generates the unique application key and writes it to APP_KEY
in the newly-created file.
How does Laravel’s config/ directory work?
One of my favorite “so beautiful in its simplicity” features within Laravel is its method of handling application configuration: each file in config/
returns an array, which can be nested as much as we need them to be.
For example, config/database.php
looks something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
<?php use Illuminate\Support\Str; return [ 'default' => env('DB_CONNECTION', 'mysql'), 'connections' => [ 'sqlite' => [ 'driver' => 'sqlite', 'url' => env('DATABASE_URL'), 'database' => env('DB_DATABASE', database_path('database.sqlite')), 'prefix' => '', 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), ], 'mysql' => [ 'driver' => 'mysql', 'url' => env('DATABASE_URL'), 'host' => env('DB_HOST', '127.0.0.1'), 'port' => env('DB_PORT', '3306'), 'database' => env('DB_DATABASE', 'forge'), 'username' => env('DB_USERNAME', 'forge'), 'password' => env('DB_PASSWORD', ''), 'unix_socket' => env('DB_SOCKET', ''), 'charset' => 'utf8mb4', 'collation' => 'utf8mb4_unicode_ci', 'prefix' => '', 'prefix_indexes' => true, 'strict' => true, 'engine' => null, 'options' => extension_loaded('pdo_mysql') ? array_filter([ PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), ]) : [], ], ], // ... and more. ]; |
I’ve truncated the file quite a bit for readability, but there are a few important points:
First, this file is config/database.php
, which will become available to us within our app as config('database')
. Similarly, we could create config/steve.php
and it would be available at config('steve')
.
We can further access values with the config()
helper and “dot”notation”; to retrieve the value of ['connections']['mysql']['url']
, we can call config('database.connections.mysql.url')
.
Next, notice that we’re using the env()
helper, which basically says “find the environment variable with this name and return it’s value; if we can’t find it, return a default value (or null
).”
This combination can be really powerful: when Laravel needs to determine what the default database engine is, it might call config('database.default')
— if the .env
file has declared a value for DB_CONNECTION
, then that value will be used; otherwise, Laravel will use “mysql”.
Avoid parsing environment variables outside of the configuration
A word of warning: in production environments, it’s recommended to cache configuration values rather than re-read them from the environment upon every request.
When such a cache file is present, Laravel will not attempt to parse the .env
file, which can lead to unexpected results.
For example, my friend’s application included the following line in the app’s main layout file:
1 |
<title>@yield('title') | {{ env('APP_NAME') }}</title> |
In development, this was fine, and his pages would render the value of the APP_NAME
environment variable in the <title />
element of each page.
Were he to deploy this to production and enable configuration caching, however, the app name would start coming back empty because his .env
file would no longer be parsed!
Instead, the call to env('APP_NAME')
should be replaced with config('app.name')
, which reads the “name” key from config/app.php
.
The right place to store third-party credentials
It’s not uncommon for Laravel applications to interface with third-party [micro-]services and APIs, and Laravel provides a stock configuration file for this very purpose: config/services.php
.
From the inline documentation at the top of that file:
This file is for storing the credentials for third party services such as Mailgun, Postmark, AWS and more. This file provides the de facto location for this type of information, allowing packages to have a conventional file to locate the various service credentials.
Before you rush to create, for example, config/geocoding.php
with just the public/private keypair from our example above, consider adding these credentials to config/services.php
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
return [ /** * Configuration for example.com's geocoding API. * * @link https://example.com/register */ 'geocoding' => [ 'endpoint' => env('EXAMPLECOM_GEOCODING_API_ENDPOINT', 'https://example.com/api'), 'public_key' => env('EXAMPLECOM_GEOCODING_PUBLIC_KEY'), 'private_key' => env('EXAMPLECOM_GEOCODING_PRIVATE_KEY'), ], // Other services... ] |
With this array defined, we can access these configuration values as needed throughout our codebase through calls like config('services.geocoding.endpoint')
.
You’ll notice that we’re using the env()
helper in two ways:
- If the
EXAMPLECOM_GEOCODING_API_ENDPOINT
environment variable is set, use that value. Otherwise, default to “https://example.com/api”. - For the API credentials, we’re not providing default values; if the developer hasn’t configured these keys, we want the requests to fail.
- The integration with the third-party API should make clear that missing API credentials are to blame for the failed request.
Summary
A lot of the mistakes my friend made are pretty common, especially for developers just getting started with application development (regardless of framework). Heed the following advice to keep your secrets…well, secret:
- Keep sensitive information — application keys, API keys, passwords, etc. — out of version control.
- Add the relevant file(s) to your
.gitignore
file to ensure nobody else can check them in, either
- Add the relevant file(s) to your
- Provide an
.env.example
file that tells other developers where to get the credentials they might not already have.- This might be a service they need to sign up for, someone in the organization they need to talk to, and/or a secure place (such as a password vault) where they need to look.
- Avoid using
env()
outside of theconfig/
directory; instead, define configuration values based on the environment variables and reference those values throughout the codebase using theconfig()
helper. - Store references to third-party services within
config/services.php
to make it clear at a glance which services are being used and set reasonable defaults without exposing sensitive information.
Leave a Reply