URL Encryption and Decryption in Magento 2

If you've ever wanted to conceal the name or location of something, look into the encrypt & decrypt functionality of Magento 2.

Mark Shust

Mark Shust

Last Updated 13 min read

As you are developing your Magento 2 code, security is a top priority. Especially when your client has sensitive information, such as if a business has confidential IP that needs protection.

The security of website URLs is an easy place to overlook. If you would like to serve up private data and don’t encrypt your URLs, it can give hackers access to that data. For example, if you’d like to give customers access to a private document without revealing the underlying location on your media folder, this is when URL encryption comes into play.

URL encryption basically converts those plain text web addresses into an encrypted format, full of gobbledygook characters. Even if someone gets their hands on one of your encrypted URLs, they wouldn't be able to make heads or tails of what the URL leads to. This helps protect the location of this URL from unsuspecting hands.

Luckily, URL encryption in Magento 2 is not that hard to code, as there are already many classes & methods that exist to allow you to easily implement this functionality. Let’s do this.

Understanding how URL encryption works in Magento

Encryption is simply when a sender scrambles a message into code, so that only the intended recipient can decrypt it. URL encryption works the same way! It just transforms easy-to-read web addresses into a jumbled up URL (encryption), and the code that accepts that request unjumbles the code to its original message (decryption).

In Magento 2, there are two primary interfaces that are responsible for encryption:

  • EncryptorInterface - Converts or scrambles up the URL into encrypted strings that are hard to interpret.
  • EncoderInterface - Encodes the encrypted URL. You can think of encoding as putting the message into a format for safe transportation.

Imagine the URL is a book. The EncryptorInterface translates the book into an ancient Egyptian language that few can understand. So far, so good - the content is encrypted. Then the EncoderInterface comes along and converts the Egyptian book into a secret code, like dots and dashes. This encoded format keeps the contents hidden while allowing it to be safely transported.

The end result is a URL that is both encrypted AND encoded for maximum security. Pretty cool right? When someone tries to access the URL, all they see is gibberish.

Protecting file downloads in Magento 2 with encryption

Let's look at a real-world example to see URL encryption working its magic in Magento 2.

Imagine you have a downloadable PDF that you want only specific customers to be able to access. For example, a downloadable PDF file would typically be available at a URL such as https://example.com/pub/media/pdf/my-secret-file.pdf.

When a customer clicks the download button from an email, it links directly to that file which exposes its location on the server. That’s definitely not ideal, and we can use URL encryption to handle this use-case.

Rather than seeing the final URL, the customer would see a URL like https://example.com/secret?f=MDozOi9MNGF0amlkT0MrbXkwVURqbEo5NUFIV2dLRkdTZ1diTzZ4T0JXaTBQblUyb3RndUpMcGgzQlpXMjNvRktRPT0~. While it’s gibberish to them, this encrypted URL still leads to respective file that we’d like to provide access to. Behind the scenes, Magento uses the EncryptorInterface and EncoderInterface interfaces that we just talked about to obfuscate the download link.

Disguise file as encrypted URL

The end result is that your file path stays protected, even as customers access the downloaded files that they have access to. This URL encryption allows you to serve up downloadable files in a custom way without compromising security.

Next let’s take a look at some implementation details 🤓

In order to create a link for the customer, we’ll first need to grab the filename, and create an encrypted & encoded link. Remember, we’d like to respond to the request from an encrypted URL that looks something like https://example.com/secret?f=MDozOi9MNGF0amlkT0MrbXkwVURqbEo5NUFIV2dLRkdTZ1diTzZ4T0JXaTBQblUyb3RndUpMcGgzQlpXMjNvRktRPT0~.

The URL encryption can be implemented where you're generating the download link for your users. To be specific, this could be something like a ViewModel, Model, or Controller class inside your custom Magento 2 module.

Here’s an example of a ViewModel that contains some code on how to achieve this:

app/code/Macademy/SecretUrl/ViewModel/Url.php
<?php

declare(strict_types=1);

namespace Macademy\SecretUrl\ViewModel;

use Magento\Framework\Encryption\EncryptorInterface;
use Magento\Framework\Url\EncoderInterface;
use Magento\Framework\UrlInterface;
use Magento\Framework\View\Element\Block\ArgumentInterface;

class Url implements ArgumentInterface
{
    public function __construct(
        private readonly EncryptorInterface $encryptor,
        private readonly EncoderInterface $urlEncoder,
        private readonly UrlInterface $url,
    ) {}

    private function getSecretPath($filename): string
    {
        $encryptedFilename = $this->encryptor->encrypt($filename);

        return $this->urlEncoder->encode($encryptedFilename);
    }

    public function getSecretUrl($filename): string
    {
        $secretPath = $this->getSecretPath($filename);

        return $this->url->setQueryParam('f', $secretPath)->getUrl('secret');
    }
}

The getSecretPath() function will return the encrypted and encoded filename (i.e. MDozOi9MNGF0amlkT0MrbXkwVURqbEo5NUFIV2dLRkdTZ1diTzZ4T0JXaTBQblUyb3RndUpMcGgzQlpXMjNvRktRPT0~), and getSecretUrl() will return the full URL (i.e. https://example.com/secret?f=MDozOi9MNGF0amlkT0MrbXkwVURqbEo5NUFIV2dLRkdTZ1diTzZ4T0JXaTBQblUyb3RndUpMcGgzQlpXMjNvRktRPT0~). Since this function isn't called from anywhere else than from within the getSecretPath() function, we will mark it as private.

By breaking up the code into two functions like this, it keeps our code abiding by the single responsibility principle, which keeps functions very small and ensures they only do one thing. This all makes our code easier to understand, test, and reason about.

Now that we have code that will generate secret links, we can call it from the place we wish to display them. To keep things simple, we’ll just display the link on the My Account page. This ensures that only logged-in customers will be able to see this link.

We’ll call the ViewModel that we created from within simple block template:

app/code/Macademy/SecretUrl/view/frontend/templates/encrypted-encoded-url.phtml
<?php
/** @var Magento\Framework\View\Element\Template $block */
/** @var Macademy\SecretUrl\ViewModel\Url $urlViewModel */
$urlViewModel = $block->getData('url_view_model');
$filename = 'my-secret-file.pdf';
$secretPath = $urlViewModel->getSecretUrl($filename);
?>
<div class="block block-encrypted-encoded-url">
    <div class="block-title">
        <strong><?= __('Your Special URL') ?></strong>
    </div>
    <div class="block-content">
        <p><a href="<?= $secretPath ?>" target="_blank"><?= $filename ?></a></p>
    </div>
</div>

Note how we are hard-coding the file name of my-secret-file.pdf into our block template. This may suffice, but your code will probably require some more advanced logic for this depending upon your client’s needs.

Then to get this block template to show on the My Account page, we can just add it to the layout XML with:

app/code/Macademy/SecretUrl/view/frontend/layout/customer_account_index.xml
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceContainer name="main">
            <block name="encrypted.encoded.url" template="Macademy_SecretUrl::encrypted-encoded-url.phtml">
                <arguments>
                    <argument name="url_view_model" xsi:type="object">Macademy\SecretUrl\ViewModel\Url</argument>
                </arguments>
            </block>
        </referenceContainer>
    </body>
</page>

This will now generate an encrypted and encoded URL for this my-secret-file.pdf file, and display all of it nicely on the My Account page:

My secret file link

The regular filename is displayed on the page, however, you’ll notice that when the cursor hovers over the link, the encrypted and encoded URL of https://example.com/secret?f=MDozOi9MNGF0amlkT0MrbXkwVURqbEo5NUFIV2dLRkdTZ1diTzZ4T0JXaTBQblUyb3RndUpMcGgzQlpXMjNvRktRPT0~ will be displayed in the status bar.

Create the controller

Clicking on the link will return a 404 error page, since there is no controller set up to listen to requests sent to /secret. To do this, we will need to create a controller for this /secret path, and we can access the f parameter within it with the RequestInterface.

Let’s set up this controller, and return a some raw contents that just outputs “secret”. This will lay the groundwork for our upcoming response that will return the contents of our secret file.

app/code/Macademy/SecretUrl/Controller/Index/Index.php
<?php

declare(strict_types=1);

namespace Macademy\SecretUrl\Controller\Index;

use Magento\Framework\App\Action\HttpGetActionInterface;
use Magento\Framework\Controller\Result\Raw;
use Magento\Framework\Controller\Result\RawFactory;

class Index implements HttpGetActionInterface
{
    public function __construct(
        private readonly RawFactory $rawFactory,
    ) {}

    public function execute(): Raw
    {
        /** @var Raw $forward */
        $raw = $this->rawFactory->create();
        return $raw->setContents('secret');
    }
}

...then we will make this page available to the /secret route with:

app/code/Macademy/SecretUrl/etc/frontend/routes.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
    <router id="standard">
        <route id="secret" frontName="secret">
            <module name="Macademy_SecretUrl"/>
        </route>
    </router>
</config>

Now when we click the secret link from the My Account page, the text "secret" will be output to the screen, which lets us know that our route and controller has been properly set up.

Encrypting and encoding the filename

Since we have all the boilerplate set up to handle the request and response flow, we’ll add in the encryption and encoding logic. This will also create a few more dependencies within the constructor.

Remember that the path we are building this logic for is /secret?f=MDozOi9MNGF0amlkT0MrbXkwVURqbEo5NUFIV2dLRkdTZ1diTzZ4T0JXaTBQblUyb3RndUpMcGgzQlpXMjNvRktRPT0~ , so we will need to grab the f parameter from the request. But more importantly, rather than redirecting the user to pub/media/my-secret-file.pdf, we grab the contents of the file instead, and then trigger the browser to download the file directly from the filesystem.

We can do this by implementing the File class, also adding in some additional logic to account for files that are not found on the filesystem:

app/code/Macademy/SecretUrl/Controller/Index/Index.php
<?php

declare(strict_types=1);

namespace Macademy\SecretUrl\Controller\Index;

use Exception;
use Magento\Framework\App\Action\HttpGetActionInterface;
use Magento\Framework\App\Filesystem\DirectoryList;
use Magento\Framework\App\RequestInterface;
use Magento\Framework\App\Response\Http\FileFactory;
use Magento\Framework\App\ResponseInterface;
use Magento\Framework\Encryption\EncryptorInterface;
use Magento\Framework\Exception\FileSystemException;
use Magento\Framework\Exception\NotFoundException;
use Magento\Framework\Filesystem;
use Magento\Framework\Filesystem\Driver\File as FileReader;
use Magento\Framework\Url\DecoderInterface;

class Index implements HttpGetActionInterface
{
    public function __construct(
        private readonly RequestInterface $request,
        private readonly DecoderInterface $urlDecoder,
        private readonly EncryptorInterface $encryptor,
        private readonly Filesystem $filesystem,
        private readonly FileReader $fileReader,
        private readonly FileFactory $fileFactory,
    ) {}

    /**
     * @throws NotFoundException
     * @throws FileSystemException
     * @throws Exception
     */
    public function execute(): ResponseInterface
    {
        $f = $this->request->getParam('f');
        $decodedFilename = $this->urlDecoder->decode($f);
        $filename = $this->encryptor->decrypt($decodedFilename);
        $mediaPath = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA)->getAbsolutePath();
        $fileAbsolutePath = $mediaPath . $filename;

        if (!$this->fileReader->isExists($fileAbsolutePath)) {
            throw new NotFoundException(__('File not found.'));
        }

        $fileContent = $this->fileReader->fileGetContents($fileAbsolutePath);

        return $this->fileFactory->create(
            $filename,
            $fileContent,
            DirectoryList::MEDIA,
        );
    }
}

After this update, you can click the link and this code will be executed. The f parameter will first be decoded and then decrypted, which is the reverse order of how we first encrypted and then decoded the filename. After the encryptor decrypts the decoded filename, the $filename variable will be equal to my-secret-file.pdf, our original file name.

As long as a file exists at pub/media/my-secret-file.pdf, the file will immediately start downloading in the new browser window that opens, while still concealing the path of the underlying path to the file on the filesystem.

Explanation of Interfaces and Dependencies

Let's recap how all the different pieces fit together to enable Magento's URL encryption feature. Each plays an important role in the process:

  • The Url view model class is where the secret URL generation begins. Its getSecretUrl() method takes a filename and passes it to the EncryptorInterface to scramble it.
  • The EncoderInterface then encodes the encrypted result to further obfuscate it. This encrypted, encoded string becomes the basis of our secret URL.
  • The Index controller class handles what happens when a user clicks the link. The DecoderInterface decodes the URL back to encrypted form.
  • The EncryptorInterface then decrypts it to reveal the original filename again.
  • With the true filename, the controller utilizes Magento's Filesystem class to retrieve the file contents from media storage.
  • Finally, the FileFactory class creates a response to deliver the file to the user's browser.
  • So in summary, the view model leverages the encryptor and encoder to generate an obfuscated URL from a filename. When clicked, the Index controller decrypts the URL to uncover the filename again and serve up the private file.
  • The EncryptorInterface handles encrypting and decrypting while the EncoderInterface provides an extra security layer during transmission. Together with routing and filesystem classes, they allow securely accessing private content.

All of these classes cooperate to encrypt URLs, decode them back to normal, and securely deliver the requested file. By orchestrating these Magento tools, we can truly lock down access to files that we want to be protected.

Resources to reference

We've just covered a ton of ground on locking down URLs in Magento 2. Consider your Magento site's links safely encrypted. If you'd like to download the full source code in this article you can get it on GitHub:

Download the code on GitHub

But the fun in programming is that there's always more to learn if you're curious! Here are some awesome resources in case you want to nerd out more on Magento encryption:

  1. Adobe Commerce Developer Documentation - The official bible for all things Magento & Adobe Commerce development. If you ever have a specific class or method question, this is the first place to look.
  2. Magento Stack Exchange — A goldmine of information with help from fellow Magento folks in a Q&A format. Search existing encryption questions or ask your own!
  3. Adobe Commerce & Magento Open Source Forums — A community of Magento professionals, enthusiasts, and beginners.
  4. Campus — A premium Magento community with access to a dedicated Magento guru, only available to University students. This question was actually originally asked and answered on Campus!
  5. Magento 2 Coding Kickstart — If any of the above code tripped you up, this premium course will help things make sense for you really quickly, so you can get real, actual work done.

The key is to keep asking questions and tinkering 🤓. Good luck!