Let's Encrypt wrapper for Laravel

Overview

Let's Encrypt Laravel

Latest Version on Packagist GitHub Tests Action Status Total Downloads

A Laravel package for easily generating and renewing SSL certificates using Let's Encrypt. This package is especially useful if you have a Laravel application that manages the SSL certificates of many domains. This package is not recommended if you just need to generate a single SSL certificate for your application.

This package is essentially a Laravel-friendly wrapper around Acme PHP.

Installation

You can install the package via composer:

composer require daanra/laravel-lets-encrypt

If you're having installation problems with conflicting dependencies caused by Guzzle then you might want to run:

composer require daanra/laravel-lets-encrypt guzzlehttp/guzzle:^6.0  -w

Publish the configuration file and the migration:

php artisan vendor:publish --provider="Daanra\LaravelLetsEncrypt\LetsEncryptServiceProvider" --tag="lets-encrypt"

Run the migration:

php artisan migrate

Note:

You somehow have to return a stored challenge whenever it it retrieved from the /.well-known/acme-challenge endpoint. You could do this by configuring NGINX/Apache appropriately or by registering a route:

Route::get('/.well-known/acme-challenge/{token}', function (string $token) {
    return \Illuminate\Support\Facades\Storage::get('public/.well-known/acme-challenge/' . $token);
})

Sometimes the /.well-known/ prefix is disabled by default in the NGINX/Apache config (see #4). Make sure it is forwarded to your Laravel application if you want Laravel to return the challenge.

Usage

Creating a new SSL certificate for a specific domain is easy:

// Puts several jobs on the queue to handle the communication with the lets-encrypt server
[$certificate, $pendingDispatch] = \Daanra\LaravelLetsEncrypt\Facades\LetsEncrypt::create('mydomain.com');

// You could, for example, chain some jobs to enable a new virtual host
// in Apache and send a notification once the website is available
[$certificate, $pendingDispatch] = \Daanra\LaravelLetsEncrypt\Facades\LetsEncrypt::create('mydomain.com', [
    new CreateNewApacheVirtualHost('mydomain.com'), 
    new ReloadApache(),
    new NotifyUserOfNewCertificate(request()->user()),
]);

// You can also do it synchronously:
\Daanra\LaravelLetsEncrypt\Facades\LetsEncrypt::createNow('mydomain.com');

Alternative syntax available from v0.3.0:

LetsEncrypt::certificate('mydomain.com')
            ->chain([
                new SomeJob()
            ])
            ->delay(5)
            ->retryAfter(4)
            ->setTries(4)
            ->setRetryList([1, 5, 10])
            ->create(); // or ->renew()

Where you can specify values for all jobs:

  • tries (The number of times the job may be attempted)
  • retryAfter (The number of seconds to wait before retrying the job)
  • retryList (The list of seconds to wait before retrying the job)
  • chain (Chain some jobs after the certificate has successfully been obtained)
  • delay (Set the desired delay for the job)

You could also achieve the same by using an artisan command:

php artisan lets-encrypt:create -d mydomain.com

Certificates are stored in the database. You can query them like so:

// All certificates
\Daanra\LaravelLetsEncrypt\Models\LetsEncryptCertificate::all();
// All expired certificates
\Daanra\LaravelLetsEncrypt\Models\LetsEncryptCertificate::query()->expired()->get();
// All currently valid certificates
\Daanra\LaravelLetsEncrypt\Models\LetsEncryptCertificate::query()->valid()->get();
// All certificates that should be renewed (because they're more than 60 days old)
\Daanra\LaravelLetsEncrypt\Models\LetsEncryptCertificate::query()->requiresRenewal()->get();

// Find certificate by domain
$certificate = LetsEncryptCertificate::where('domain', 'mydomain.com')->first();
// If you no longer need it, you can soft delete
$certificate->delete();
// Or use a hard delete
$certificate->forceDelete();

Failure events

If one of the jobs fails, one of the following events will be dispatched:

Daanra\LaravelLetsEncrypt\Events\CleanUpChallengeFailed
Daanra\LaravelLetsEncrypt\Events\ChallengeAuthorizationFailed
Daanra\LaravelLetsEncrypt\Events\RegisterAccountFailed
Daanra\LaravelLetsEncrypt\Events\RequestAuthorizationFailed
Daanra\LaravelLetsEncrypt\Events\RequestCertificateFailed
Daanra\LaravelLetsEncrypt\Events\StoreCertificateFailed
Daanra\LaravelLetsEncrypt\Events\RenewExpiringCertificatesFailed

Every event implements the Daanra\LaravelLetsEncrypt\Interfaces\LetsEncryptCertificateFailed interface so you can listen for that as well.

Automatically renewing certificates

Certificates are valid for 90 days. Before those 90 days are over, you will want to renew them. To do so, you could add the following to your App\Console\Kernel:

protected function schedule(Schedule $schedule)
{
    $schedule->job(new \Daanra\LaravelLetsEncrypt\Jobs\RenewExpiringCertificates)->daily();
}

This will automatically renew every certificate that is older than 60 days, ensuring that they never expire.

Configuration

By default this package will use Let's Encrypt's staging server to issue certificates. You should set:

LETS_ENCRYPT_API_URL=https://acme-v02.api.letsencrypt.org/directory

in the .env file of your production server.

By default, this package will attempt to validate a certificate using a HTTP-01 challenge. For this reason, a file will be temporarily stored in your application's storage directory under the path app/public/.well-known/acme-challenge/<CHALLENGE_TOKEN>. You can customise this behavior by setting a custom PathGenerator class in your config under path_generator. Note that Let's Encrypt expects the following path:

/.well-known/acme-challenge/<CHALLENGE_TOKEN>

to return the contents of the file located at $pathGenerator->getPath($token).

Testing

composer test

Changelog

Please see CHANGELOG for more information on what has changed recently.

Contributing

Please see CONTRIBUTING for details.

Security

If you discover any security related issues, please email [email protected] instead of using the issue tracker. If you have a question, please open an issue instead of sending an email.

Credits

License

The MIT License (MIT). Please see License File for more information.

Comments
  • Challenge failing - domain routing

    Challenge failing - domain routing

    Hey there, first of all, thanks for the amazing package.

    Im trying to use it for creating ssl certs for clients of my saas app.

    Basically after users register in the app, they get a custom subdomain, in the form: client1.myapp.com, client2.myapp.com.., etc. And they can use a CNAME to use a custom domain they bring, classic SAAS.

    I configured a CNAME record which is reaching the app fine, but in this case when creating a certificate I am failing on the challenge step, not being able to validate the file in .well_known....

    My routes file looks like this:

    Route::group(['domain' => '{company:slug}.'.config('app.domain')], function () {
    	Route::group(['middleware' => 'tenant'], function(){
    		Route::get('', [HomeController::class, 'index']);
    
    ...
    ...
    

    I'm using the config file as is, only updating the api_url to use the production one.

    Checking in the server, i can't see any file created in public/.well_known.... Should I be able to see the file even if challenge fails? Maybe that's where i'm having the issue so the file is never uploaded...

    Thanks in advance for any help!

    opened by dmarcos89 10
  • Certificate not creating please help me

    Certificate not creating please help me

    root@files:/home/fs/public_html# php artisan lets-encrypt:create -d edulife.cmhlmc.com Generating certificates for 1 domains. edulife.cmhlmc.com: Failed to generate a certificate for edulife.cmhlmc.com Challenge failed (response: {"type":"http-01","status":"invalid","error":{"type":"urn:ietf:params:acme:error:unauthorized","detail":"66.45.249.26: Invalid response from http://edulife.cmhlmc.com/.well-known/acme-challenge/qCfxYAtN2NeQPm5WT1EIPTO5DBOX_u9YW3pp8VxChao: 404","status":403},"url":"https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/3123418344/UPLhFg","token":"qCfxYAtN2NeQPm5WT1EIPTO5DBOX_u9YW3pp8VxChao","validationRecord":[{"url":"http://edulife.cmhlmc.com/.well-known/acme-challenge/qCfxYAtN2NeQPm5WT1EIPTO5DBOX_u9YW3pp8VxChao","hostname":"edulife.cmhlmc.com","port":"80","addressesResolved":["66.45.249.26"],"addressUsed":"66.45.249.26"}],"validated":"2022-07-25T10:12:37Z"}).

    opened by mmejaz786 7
  • Fail to generate a KeyPair with the given options

    Fail to generate a KeyPair with the given options

    Hello, I tried running the php artisan command but it returns the following error:

    error

    I saw the other thread with this error that was resolved by installing OpenSSL. I have got it enabled and php says it is enabled with a path to a openssl.cnf. The thing is that the path doesn't actually exist.

    openssl

    I don't know if that is why the command doesn't work? I am very new with certificates...

    opened by XavegX367 4
  • Scope problem when running renew from a callback

    Scope problem when running renew from a callback

    I am struggling to find a solution for a scope problem in the Laravel when you execute the command from the callback. From the Laravel command, I am trying to execute the job in a different context (multi-tenant platform, changing database and other settings). However, no matter what I tried, I all the time getting the error from the package. Reading through PHP manuals, StackOverflow and other articles, I tried bunch of different things such as:

    I must emphasize that I do not have a problem with scope in general - I have numerous commands which are executed in a dynamic context and everything runs without any problem. This error occurs only when I try to use this package in a callback

    Expected result: Running job in correct Laravel context (changed by setup method in Instance model).

    Actual result:

    Using $this when not in object context at app/Models/Instance.php:80

    Let me show you the problem:

    Command:

    class RenewExpiringCertificates extends Command {
      public function handle(): int {
          LetsEncryptCertificate::query()
              ->requiresRenewal()
              ->chunk(100, function (LetsEncryptCertificateCollection $certificates) {
                  $certificates->renewNow();  // PROBLEMATIC CALL
              });
    
          return 0;
      }
      
      protected function execute(InputInterface $input, OutputInterface $output) {
          $method = method_exists($this, 'handle') ? 'handle' : '__invoke';
    
          $count = 0;
          Instance::inAllActiveSpaces(function () use ($method, &$count) {
              $count += $this->laravel->call([$this, $method]); // CALL TO HANDLE METHOD
          });
    
          return $count;
      }
    }
    

    Instance:

    class Instance extends Model {
        protected $connection = 'mysql-master';
    
        public function setup() {
            Config::set('database.connections.mysql.database', $this->database);
            DB::reconnect('mysql');
            // ...
        }
    
        public static function inAllActiveSpaces($callback) {
            Instance::active()->each(function ($instance) use ($callback) {
                $instance->setup();
                $callback($instance);  // BREAKING POINT
            });
        }
    }
    

    From my research and looking into the source code, this looks to raise a problem:

    // Daanra\LaravelLetsEncrypt\Models\LetsEncryptCertificate
    public function renewNow(): self
    {
        return LetsEncrypt::renewNow($this);
    }
    
    opened by kristijan97 4
  • Support for laravel 9 and php 8

    Support for laravel 9 and php 8

    Kindly add support for laravel 9 & php 8

    Your requirements could not be resolved to an installable set of packages.

    Problem 1 - acmephp/core[dev-master, 1.3.0] require guzzlehttp/psr7 ^1.0 -> found guzzlehttp/psr7[1.0.0, ..., 1.x-dev] but the package is fixed to 2.2.1 (lock file version) by a partial update and that version does not match. Make sure you list it as an argument for the update command. - acmephp/core 1.2.0 requires guzzlehttp/guzzle ^6.0 -> found guzzlehttp/guzzle[6.0.0, ..., 6.5.x-dev] but it conflicts with your root composer.json require (^7.0.1). - daanra/laravel-lets-encrypt v0.2.4 requires acmephp/core ^1.2|dev-master -> satisfiable by acmephp/core[dev-master, 1.2.0, 1.3.0]. - Root composer.json requires daanra/laravel-lets-encrypt ^0.2.4 -> satisfiable by daanra/laravel-lets-encrypt[v0.2.4].

    Use the option --with-all-dependencies (-W) to allow upgrades, downgrades and removals for packages currently locked to specific versions. You can also try re-running composer require with an explicit version constraint, e.g. "composer require daanra/laravel-lets-encrypt:*" to figure out if any version is installable, or "composer require daanra/laravel-lets-encrypt:^2.1" if you know which you need.

    opened by manuelfrans 4
  • Adding subjectAlternativeNames support

    Adding subjectAlternativeNames support

    As discussed in #22, this PR adds subjectAlternativeNames support.

    The following is currently untested, but just wanted to check direction before I go any further.

    opened by Muffinman 3
  • [Feature Request] Subject Alternative Names

    [Feature Request] Subject Alternative Names

    Hi,

    I'm wondering if this package supports sending SubjectAlternativeNames to LetsEncrypt / AcmePHP?

    I can see that AcmePHP itself does have support for them, but I couldn't see any obvious references in this package.

    EDIT: Our use case is for a multi-tenant Laravel application. we want one vhost per tenant rather than one per domain, so it makes sense to have a single SSL cert with all their active domains contained inside.

    opened by Muffinman 3
  • NGINX configuration

    NGINX configuration

    Please share correct nginx configuration. Default Laravel / nginx / apache install give 404 Not Found error when trying to access https://mydomain.com/.well-known/acme-challenge/any8Token8Typed8Here.

    If I'm creating Laravel route with "dot" before "well-know" as it requires lets encrypt:

    Route::get('/.well-known/acme-challenge/{token}', function (string $token) { <<<=== it doesn't work

    Removing dot makes routes reachable but it is not something what lets encrypt expects:

    Route::get('/well-known/acme-challenge/{token}', function (string $token) { <<<=== it works

    For some reason /.well-known/acme-challenge/{token} is not served by laravel application, nginx doesn't allow this route. (But for example allows next: "/.well-known/acme-challenge")

    Approach no. 2:

    If nginx settings are not available, please help to understand how to put / return token from default "/public/.well-known/acme-challenge/....." folder

    In documentation I clearly see: "You can customise this behavior by setting a custom PathGenerator class in your config under path_generator', but no further explanation how to create custom PathGenerator class.

    opened by e8dev 3
  • Does this work for internally used domains that are not accessible by networks outside of our own network

    Does this work for internally used domains that are not accessible by networks outside of our own network

    I was wondering if it is possible o use this package to create SSL certificates for internally used domains that are not accessible from outside of our network. The domain is set using our own local DNS Server.

    Right now when I try to create a certificate it returns the following:

    Challenge failed (response: {"type":"http-01","status":"invalid","error":{"type":"urn:ietf:params:acme:error:unauthorized","detail":"91.184.0.100: Invalid response from http://labelsolutions..........nl/.well-known/acme-challenge/w9Js1qFOMkH9LH2Bp6jTJBZFNq79KM0ehuHuJ92MSd0: 404","status":403},"url":"https://acme-v02.api.letsencrypt.org/acme/chall-v3/174525676347/Y0Wtbw","token":"w9Js1qFOMkH9LH2Bp6jTJBZFNq79KM0ehuHuJ92MSd0","validationRecord":[{"url":"http://labelsolutions............nl/.well-known/acme-challenge/w9Js1qFOMkH9LH2Bp6jTJBZFNq79KM0ehuHuJ92MSd0","hostname":"labelsolutions..........nl","port":"80","addressesResolved":["91.184.0.100"],"addressUsed":"91.184.0.100"}],"validated":"2022-11-10T09:56:36Z"}).

    I replaced the original domain name for dots, but the url for us internally is working.

    opened by XavegX367 2
  • Pb with jobs

    Pb with jobs

    Hello,

    I don't understand how create jobs after successfully generate certificate. For example, I would like to create an apache virtualhost and add a row in database. I found this on website but I don't understand where is this job ?

    [$certificate, $pendingDispatch] = \Daanra\LaravelLetsEncrypt\Facades\LetsEncrypt::create('mydomain.com', [ new CreateNewApacheVirtualHost('mydomain.com'), new ReloadApache(), new NotifyUserOfNewCertificate(request()->user()), ]);

    Where is job CreateNewApacheVirtualHost ? How create this please ?

    opened by alexandre30290 2
  • In which php version this package was tested?

    In which php version this package was tested?

    Hi, thank you fro the awesome package.

    I have tried to install the package in laravel 5.5 project but not working. so i am wondering in which php version it was tested?

    if you wondering what the errors: PHP Error: Call to undefined method Daanra/LaravelLetsEncrypt/Jobs/RegisterAccount::dispatchNow() second error BadMethodCallException with message 'Method ensureDirectoryExists does not exist.

    opened by abublihi 2
Releases(v.0.5.0)
Owner
Daan Raatjes
Daan Raatjes
An open source tool that lets you create a SaaS website from docker images in 10 minutes.

简体中文 Screenshots for members ( who subscribe the plan ) for admin ⚠️ This document was translated into English by deepl and can be improved by PR An o

Easy 669 Jan 5, 2023
This simple code lets you to get how many % of this year had passed

Year-Percent-PHP This simple code lets you to get how many % of this year had passed Code is part of Telegram bot and was made for Telegram channel Ye

Puzzak 1 Jan 3, 2022
A PocketMine-MP plugin that lets you teleport among offline players

OfflinePlayerTP A PocketMine-MP plugin that lets you teleport among offline players. Commands Command Description /otp <player> Teleport to (offline)

Muqsit Rayyan 8 Sep 23, 2022
This Pocketmine-MP plugin lets you implement the ultimate birthday wishing system on your server.

BirthdaysPE This Pocketmine-MP plugin will let you wish player(s) a happy birthday and notify others to wish them too. Commands /birthday <set/reset>

MCA7 3 Jul 25, 2022
LendCash is a cash lending service that lets you take loans against your stocks portfolio value and pay back on a prorated basis.

LendCash is a cash lending service that lets you take loans against your stocks portfolio value and pay back on a prorated basis.

Teniola Fatunmbi 2 Aug 22, 2022
A complete solution for group projects in organizations that lets you track your work in any scenario. Working in a team is a cumbersome task, ease it using our project management system.

SE-Project-Group24 What is Evolo? Evolo is Dashboard based Project Management System. A complete solution for group projects in organizations that let

Devanshi Savla 2 Oct 7, 2022
A Laravel Wrapper for the Binance API. Now easily connect and consume the Binance Public & Private API in your Laravel apps without any hassle.

This package provides a Laravel Wrapper for the Binance API and allows you to easily communicate with it. Important Note This package is in early deve

Moinuddin S. Khaja 7 Dec 7, 2022
This package provides a wrapper for Google Lighthouse to audit the quality of web pages with Laravel.

laravel-google-lighthouse This package is based on octoper/lighthouse-php. This package provides a wrapper for Google Lighthouse to audit the quality

Logiek 5 Jun 1, 2022
nmap is a PHP wrapper for Nmap.

nmap nmap is a PHP wrapper for Nmap, a free security scanner for network exploration. Usage use Nmap\Nmap; $hosts = Nmap::create()->scan([ 'williamdur

William Durand 151 Sep 24, 2022
A simple API with Guzzle wrapper, providing easy access to wppconnect's endpoints.

WPPConnect Team Wppconnect Laravel Client A simple API with Guzzle wrapper, providing easy access to wppconnect's endpoints. Requirements PHP 7.4 or n

null 28 Dec 18, 2022
WordPlate is a wrapper around WordPress. It makes developers life easier. It is just like building any other WordPress website with themes and plugins. Just with sprinkles on top.

WordPlate is simply a wrapper around WordPress. It makes developers life easier. It is just like building any other WordPress website with themes and plugins. Just with sprinkles on top.

WordPlate 1.7k Dec 24, 2022
AI PHP is a wrapper for rubix ml to make AI very approachable

AI PHP Rubix Wrap A wrapper for Rubix ML to make it very approachable Example: $report = RubixService::train($data, 'column_with_label'); Where co

null 15 Nov 5, 2022
PhpGit - A Git wrapper for PHP 7.1+

PhpGit PhpGit - A Git wrapper for PHP 7.1+ The project is forked from https://github.com/kzykhys/PHPGit Requirements PHP 7.1+ Git Installation Update

PHPPkg 9 Nov 1, 2022
Kirby wrapper for automated content accessibility checkers Editoria11y and Sa11y

Kirby3 A11yprompter For a comprehensive overview of Sa11y and Editoria11y, how they can assist maintaining an accessible website by supporting content

Sebastian Greger 8 Apr 25, 2022
Standardized wrapper for popular currency rate APIs. Currently supports FixerIO, CurrencyLayer, Open Exchange Rates and Exchange Rates API.

?? Wrapper for popular Currency Exchange Rate APIs A PHP API Wrapper to offer a unified programming interface for popular Currency Rate APIs. Dont wor

Alexander Graf 24 Nov 21, 2022
This is a simple Wrapper around the ZipArchive methods with some handy functions

Note I haven't updated this package in a long time except merging PRs. The last time I was using this package was with PHP5. I archived the repository

Nils Plaschke 845 Dec 13, 2022
This is a simple Wrapper around the ZipArchive methods with some handy functions

Note I haven't updated this package in a long time except merging PRs. The last time I was using this package was with PHP5. I archived the repository

Nils Plaschke 836 Jan 26, 2022
Lightweight PHP wrapper for OVH APIs. That's the easiest way to use OVH.com APIs in your PHP applications.

This PHP package is a lightweight wrapper for OVH APIs. That's the easiest way to use OVH.com APIs in your PHP applications.

OVHcloud 263 Dec 14, 2022
An un-offical API wrapper for logsnag.com to get notifications and track your project events

An un-offical API wrapper for logsnag.com to get notifications and track your project events

David Oti 3 Oct 15, 2022