Tilly The Coder
Tilly The Coder
Encrypting Values in Laravel 12 — A Complete, Modern Guide
Tilly The Coder

Encrypting Values in Laravel 12 — A Complete, Modern Guide

Tilly The Coder
7 mins
This is the estimated time it takes to read the article.

Reconstructed guide, written September 26, 2026. This replaces the lost article with new material based on the Laravel 12 documentation. Its original URL is retained for existing links.

Begin with the reason a value needs to be recoverable. An application may need to send a stored provider token back to that provider, so encryption is appropriate. A login password should be verified without recovering the original password. Decide that distinction before adding a cast or choosing a database column.

Laravel documents its reversible encryption API in the Laravel 12 encryption guide. Crypt::encryptString() and decryptString() operate on strings; invalid ciphertext raises DecryptException. The application encryption key comes from configuration, normally APP_KEY.

Encrypt a string at an explicit boundary

use Illuminate\Support\Facades\Crypt;

$token = 'example-token-for-a-local-test';
$ciphertext = Crypt::encryptString($token);
$recovered = Crypt::decryptString($ciphertext);

if (! hash_equals($token, $recovered)) {
    throw new RuntimeException('Encryption round trip failed.');
}

This example exercises the API without printing either value. In application code, perform the operation inside the action that stores or uses the credential. Keep credentials out of request logs, exception context, debug dumps, and analytics payloads. Encryption at rest does not prevent a later line of application code from exposing plaintext.

Use an encrypted cast for model-owned data

The encrypted-casting documentation supports encrypted strings and structured variants such as encrypted:array. Use a TEXT column or larger because ciphertext length grows. Encrypted attributes cannot be searched as ordinary database values.

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

final class ProviderConnection extends Model
{
    protected $hidden = ['access_token'];

    /** @return array<string, string> */
    protected function casts(): array
    {
        return ['access_token' => 'encrypted'];
    }
}

Create the matching access_token column through a migration before using the model. Assign plaintext through the cast and let Eloquent encrypt it. Encrypting the same value manually before assigning it would create two encryption layers and complicate every reader.

The hidden property reduces accidental exposure during model serialization. Explicit API resources should still select the public fields the client needs. Authorization must run before returning any sensitive data or using a saved credential on a user’s behalf.

Example: store a provider token through a typed action

For an integration settings screen, validate the incoming token in a custom Form Request and authorize the connection through a policy before calling this action. The action accepts the already selected model and a string, then lets the encrypted cast perform the conversion. It does not accept an arbitrary array of model attributes.

namespace App\Actions;

use App\Models\ProviderConnection;

final class UpdateProviderToken
{
    public function handle(
        ProviderConnection $connection,
        string $token,
    ): void {
        $connection->access_token = $token;
        $connection->saveOrFail();
    }
}

A settings page should show whether a token is configured, without repopulating the password-style field with the stored token. A replacement token is a deliberate update. Define a separate authorized disconnect action for removing a credential, so a blank form submission cannot ambiguously mean both “keep the current token” and “erase it”.

For a structured private preference, encrypted:array can hold a small collection of values that must be recovered together. Keep fields you need for filtering, such as an integration’s public provider name or connection status, in separate ordinary columns. Do not expect a database WHERE clause on the plaintext token to work against encrypted storage.

Verify the storage boundary, not only a round trip

A plain encrypt/decrypt check proves the encryption service works. A model test should additionally prove the cast writes ciphertext to storage and returns the original value after reloading. In a PHPUnit feature test using your project’s database refresh setup, the core assertions can look like this:

use App\Actions\UpdateProviderToken;
use App\Models\ProviderConnection;
use Illuminate\Support\Facades\DB;

$connection = new ProviderConnection;
// Fill any other columns required by your own schema before saving.
$token = 'local-test-token';

app(UpdateProviderToken::class)->handle($connection, $token);

$stored = DB::table($connection->getTable())
    ->where($connection->getKeyName(), $connection->getKey())
    ->value('access_token');

$this->assertIsString($stored);
$this->assertNotSame($token, $stored);
$this->assertSame($token, $connection->fresh()->access_token);
$this->assertArrayNotHasKey('access_token', $connection->toArray());

Use synthetic credentials in these checks. The raw database query intentionally bypasses the cast, while fresh() exercises a new model read. Together those assertions catch a missing cast, an incorrect column name, and accidental serialization exposure. They also provide a useful regression check if the model’s casting configuration changes later.

Treat a decryption error as an incident to investigate

A failed decrypt can mean the wrong key, a damaged value, or data that was never encrypted using the expected format. Preserve the original ciphertext while investigating. Do not substitute an empty token, save that value, and turn a configuration problem into permanent data loss.

Report a safe error containing a record identifier and operation name. Keep the secret and full ciphertext out of the message. If the user needs to reconnect an integration, show that explicit state only after the application has identified the cause and the required recovery path.

Plan key rotation with the data lifecycle

Laravel 12 accepts a comma-separated APP_PREVIOUS_KEYS list. New encryption uses the current key; decryption can try configured previous keys. This allows a transition period, but it does not automatically rewrite every stored value.

Before rotation, identify encrypted columns, long-lived tokens, encrypted cookies, and retained backups. Test recovery from a backup in an isolated environment with its required keys. Keep keys in the approved secret store and make access auditable. A database backup without the matching encryption material may not be recoverable.

If the goal is to retire an old key, use an explicit, resumable re-encryption process with verified counts and failure handling. Account for older backups before deleting key material. Schedule this as a controlled data operation rather than a routine deployment command.

Example: rehearse a rotation before touching production

In an isolated test, create a value with an old key, configure a new current key and the old previous key, and confirm the value remains readable. Then explicitly rewrite a sample record and verify it can be read with the new key alone. This separates compatibility during the transition from evidence that data has actually been migrated.

Process persistent records in bounded batches, record which records succeeded, and stop on an unexpected decryption failure. Consider concurrent writes: a migration should not overwrite a newly saved token with the older value it read at the start of a batch. Use the application’s transaction and locking strategy, and test that race with two separate database connections before running a large migration.

A deploy that changes environment values must also account for cached configuration and long-running workers. Verify the effective configuration inside the processes that perform encryption, without printing the keys. Otherwise, different processes can continue writing with different active keys during the transition, making the completion count misleading.

Common questions about Laravel encrypted values

Can encrypted storage replace authorization?

No. Once the running application decrypts a credential, ordinary application code can use it. A policy still needs to determine whether the requesting user may update or use the connection. For example, a team administrator may rotate a team credential while a learner can only use the resulting course feature.

Should every field be encrypted?

Start with the data requirement and threat model. Encrypting a provider token has a clear recoverability requirement. Encrypting a public course title would make ordinary filtering harder without protecting private information. Document which fields need encryption, who can access their decrypted values, and how those fields are recovered during an incident.

Keep password verification separate

Use Laravel’s Hash facade and password-hashing facilities for login passwords. Review tests for successful verification, rejected credentials, encrypted-value round trips, and tampered ciphertext. The test suite should exercise the failure paths that would otherwise only appear during a restore.