Closed BartMommens closed 3 years ago
I didn't have ever changed the key
at runtime: I think you should do the change before calling the EncryptionServiceProvider
.
I found a solution but still working out the kinks. Basically EncryptionServiceProvider
creates a Singleton at boot. So overriding the config('app.key')
will not help and the same with force registering the EncryptionServiceProvider
.
So the thing is at first you'll need main app key to decode the tenant data in the Landlord Database so at this point you're stuck with the .env APP_KEY. So what i'm doing now is getting the app key from the tenant database decoding it and storing it in a separate config var config(['tenant.app.key' => $tenantSettings->app_key]);
Now i'm going to create a new trait that loads a custom instance of Crypt and encrypt all models for tenant with this crypter to use that custom key to encrypt everything :)
Sharing code below in a sec
So here is what i did:
So the EncryptionServiceProvider is using the main APP_KEY (which encrypts all data in the landlord database). In my SwithTenantConfigTask i get the tenants app_key ( currently in a separate model due to interface thingie #250 )
SwithTenantConfigTask.php
namespace App\Tasks\System\Tenant;
use App\Models\System\Tenant\TenantSettings;
use Illuminate\Support\Facades\App;
use Spatie\Multitenancy\Tasks\SwitchTenantTask;
use Spatie\Multitenancy\Models\Tenant;
class SwitchTenantConfigTask implements SwitchTenantTask
{
public function makeCurrent(Tenant $tenant): void
{
$tenantSettings = TenantSettings::where('tenant_id', $tenant->id)->first();
config([
'tenant.app.key' => $tenantSettings->app_key,
]);
#Check key is set and was able to be decoded
if(empty(config('tenant.app.key'))){
abort('500', 'Tenant app key is missing!');
}
}
public function forgetCurrent(): void
{
config([
'tenant.app.key' => null
]);
}
}
So at this point i have a config('tenant.app.key')
.
Now here is an example of a model for a tenat being User.
namespace App\Models;
use App\Traits\EncryptTenantStrings;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Spatie\Multitenancy\Models\Concerns\UsesTenantConnection;
class User extends Authenticatable implements MustVerifyEmail
{
use HasFactory,
Notifiable,
UsesTenantConnection,
EncryptTenantStrings;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'first_name',
'last_name',
'email',
'password',
];
/**
* The attributes that should be hidden for arrays.
*
* @var array
*/
protected $hidden = [
'last_name',
'email',
'password',
'remember_token',
];
/**
* The attributes that should be cast to native types.
*
* @var array
*/
protected $casts = [
'email_verified_at' => 'datetime',
];
/**
* Set encrypted User's First Name.
*
* @param string $value
* @return string
*/
protected function setFirstNameAttribute($value)
{
$this->attributes['first_name'] = $this->encryptString($value);
}
/* DECRYPTION PART */
/**
* Get decrypted User's First Name.
*
* @param string $value
* @return string
*/
protected function getFirstNameAttribute($value)
{
return $this->decryptString($value);
}
protected function setLastNameAttribute($value)
{
$this->attributes['last_name'] = $this->encryptString($value);
}
/* DECRYPTION PART */
/**
* Get decrypted Admin's Last Name.
*
* @param string $value
* @return string
*/
protected function getLastNameAttribute($value)
{
return $this->decryptString($value);
}
}
Here i use Accessors and mutators that use a custom encrypter instance via a trait:
namespace App\Traits;
use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Encryption\Encrypter;
trait EncryptTenantStrings
{
protected $tenantEncrypter;
public function encryptString(string $plaintext): string
{
if (!empty($plaintext)) {
return $this->getTenantEncrypter()->encryptString($plaintext);
}
return $plaintext;
}
public function decryptString(string $ciphertext): string
{
if (empty($ciphertext)) {
return $ciphertext;
}
try {
return $this->getTenantEncrypter()->decryptString($ciphertext);
} catch (DecryptException $e) {
return '';
}
}
protected function getTenantEncrypter(): Encrypter
{
if (!$this->tenantEncrypter) {
$encryptionKey = base64_decode(substr(config('tenant.app.key'), 7)); //get the appkey and decode it.
$this->tenantEncrypter = new Encrypter($encryptionKey, config('app.cipher'));
}
return $this->tenantEncrypter;
}
}
This works, i'm not sure it's an optiomal way of doing but at this point it's working. Hope i can help someone with this code. Suggestions of optimizing or making it cleaner / leaner are always welcome. Also If this approach is crappy let me know :) i'll try to rethink my strat again 🙃
@BartMommens, format your code using the PHP format style syntax, please. Thanks for your share.
@masterix21 i'm trying but can't get it tho show properly i'm a github sharer noob :)
@masterix21 sorry for the wait, i have properly formatted the code :)
Thanks
Hey all,
I'm currently trying to change the
config('app.key')
dynamic per tenant.I have an extra table that contains the app keys for a specific tenant.
I'm running custom task that should switch certain config variables on runtime.
So upon switching tenant a want to switch the app.key in the config with the one in the database. when i
dd(config('app.key')
after setting it it displays the correct key. Unfortunatly some of my models have accessors to encrypt / decrypt the data. example:Here the aAPP_KEY from the environement is still being used after setting it on the tenant switch task. So all the data is still encrypted with the main key instead of the tenant's key. Also when i run Tinker for a tenant and dumpt the app.key from config the tenant key is displayed but Tinker also keeps using the Main APP_KEY.
For that reason i try to force register the EncryptionServiceProvider::class right after setting the new app.key on runtime... but that doesn't change anything.
Anyone have any idea how to make this work?