diff --git a/config/xero.php b/config/xero.php index 372fb84..3fb02bb 100755 --- a/config/xero.php +++ b/config/xero.php @@ -36,4 +36,9 @@ * Set the scopes */ 'scopes' => env('XERO_SCOPES', 'openid email profile offline_access accounting.settings accounting.transactions accounting.contacts'), -]; \ No newline at end of file + + /** + * Encrypt tokens in database? + */ + 'encrypt' => env('XERO_ENCRYPT', false), +]; diff --git a/readme.md b/readme.md index 07072d2..a5c6f15 100755 --- a/readme.md +++ b/readme.md @@ -36,6 +36,14 @@ You can publish the config file with: php artisan vendor:publish --provider="Dcblogdev\Xero\XeroServiceProvider" --tag="config" ``` +# Encrypt Tokens at rest +You can enable token encryption at rest by setting the following in your .env file: + +``` +XERO_ENCRYPT=true +``` +this will use the native Laravel Crypt library to ensure the tokens are encrypted at rest. + # Migration You can publish the migration with: diff --git a/src/Console/Commands/XeroShowAllCommand.php b/src/Console/Commands/XeroShowAllCommand.php index 0097e33..58d1a3f 100644 --- a/src/Console/Commands/XeroShowAllCommand.php +++ b/src/Console/Commands/XeroShowAllCommand.php @@ -4,6 +4,8 @@ use Illuminate\Console\Command; use Dcblogdev\Xero\Models\XeroToken; +use Illuminate\Contracts\Encryption\DecryptException; +use Illuminate\Support\Facades\Crypt; class XeroShowAllCommand extends Command { @@ -33,12 +35,34 @@ public function handle() 'tenant_id', 'updated_at', ]; + // Fetch all access tokens - $tokens = XeroToken::select($dataToDisplay)->get()->toArray(); + $tokens = XeroToken::select($dataToDisplay)->get(); + + if(config('xero.encrypt')) { + $tokens->map(function($token) { + try { + $access_token = Crypt::decryptString($token->access_token); + } catch (DecryptException $e) { + $access_token = $token->access_token; + $refresh_token = $token->refresh_token; + } + // Split them as a refresh token may not exist... + try { + $refresh_token = Crypt::decryptString($token->refresh_token); + } catch (DecryptException $e) { + $refresh_token = $token->refresh_token; + } + + $token->access_token = $access_token; + $token->refresh_token = $refresh_token; + return $token; + }); + } $this->table( $dataToDisplay, - $tokens + $tokens->toArray() ); } -} \ No newline at end of file +} diff --git a/src/Facades/Xero.php b/src/Facades/Xero.php index 2772bc8..2d2e00c 100755 --- a/src/Facades/Xero.php +++ b/src/Facades/Xero.php @@ -11,7 +11,7 @@ class Xero extends Facade * * @return string */ - protected static function getFacadeAccessor() + protected static function getFacadeAccessor(): string { return 'xero'; } diff --git a/src/Xero.php b/src/Xero.php index 85abece..ff07bb2 100755 --- a/src/Xero.php +++ b/src/Xero.php @@ -7,10 +7,12 @@ use Dcblogdev\Xero\Resources\Invoices; use Dcblogdev\Xero\Resources\Webhooks; use Exception; +use Illuminate\Contracts\Encryption\DecryptException; use Illuminate\Contracts\Foundation\Application; use Illuminate\Http\Client\RequestException; use Illuminate\Http\RedirectResponse; use Illuminate\Routing\Redirector; +use Illuminate\Support\Facades\Crypt; use Illuminate\Support\Facades\Http; use RuntimeException; @@ -74,16 +76,7 @@ public function disconnect(): void public function connect(): RedirectResponse|Application|Redirector { //when no code param redirect to Microsoft - if (! request()->has('code')) { - $url = self::$authorizeUrl . '?' . http_build_query([ - 'response_type' => 'code', - 'client_id' => config('xero.clientId'), - 'redirect_uri' => config('xero.redirectUri'), - 'scope' => config('xero.scopes') - ]); - - return redirect()->away($url); - } elseif (request()->has('code')) { + if (request()->has('code')) { // With the authorization code, we can retrieve access tokens and other data. try { $params = [ @@ -120,15 +113,42 @@ public function connect(): RedirectResponse|Application|Redirector throw new Exception($e->getMessage()); } } + + $url = self::$authorizeUrl . '?' . http_build_query([ + 'response_type' => 'code', + 'client_id' => config('xero.clientId'), + 'redirect_uri' => config('xero.redirectUri'), + 'scope' => config('xero.scopes') + ]); + + return redirect()->away($url); } public function getTokenData(): XeroToken|null { if ($this->tenant_id) { - return XeroToken::where('tenant_id', '=', $this->tenant_id)->first(); + $token = XeroToken::where('tenant_id', '=', $this->tenant_id)->first(); + } else { + $token = XeroToken::first(); + } + + if(config('xero.encrypt')) { + try { + $access_token = Crypt::decryptString($token->access_token); + } catch (DecryptException $e) { + $access_token = $token->access_token; + } + // Split them as a refresh token may not exist... + try { + $refresh_token = Crypt::decryptString($token->refresh_token); + } catch (DecryptException $e) { + $refresh_token = $token->refresh_token; + } + $token->access_token = $access_token; + $token->refresh_token = $refresh_token; } - return XeroToken::first(); + return $token; } public function getAccessToken($redirectWhenNotConnected = true): string @@ -222,10 +242,10 @@ protected function storeToken($token, $tenantData = null) { $data = [ 'id_token' => $token['id_token'], - 'access_token' => $token['access_token'], + 'access_token' => config('xero.encrypt') ? Crypt::encryptString($token['access_token']) : $token['access_token'], 'expires_in' => $token['expires_in'], 'token_type' => $token['token_type'], - 'refresh_token' => $token['refresh_token'], + 'refresh_token' => config('xero.encrypt') ? Crypt::encryptString($token['refresh_token']) : $token['refresh_token'], 'scopes' => $token['scope'] ]; diff --git a/src/database/migrations/create_xero_tokens_table.php b/src/database/migrations/create_xero_tokens_table.php index b4db364..a24844e 100755 --- a/src/database/migrations/create_xero_tokens_table.php +++ b/src/database/migrations/create_xero_tokens_table.php @@ -1,8 +1,8 @@ toBe('1234'); -}); \ No newline at end of file +}); + +test('can get tokens when not-encrypted but encryption is enabled', function () { + + Config::set('xero.encrypt', true); + + XeroToken::create([ + 'id' => 0, + 'access_token' => '1234', + 'expires_in' => strtotime('+1 day'), + 'scopes' => 'contacts' + ]); + + $data = XeroFacade::getAccessToken(); + + expect($data)->toBe('1234'); +}); + +test('can get tokens when encrypted', function () { + + Config::set('xero.encrypt', true); + + XeroToken::create([ + 'id' => 0, + 'access_token' => Crypt::encryptString('1234'), + 'expires_in' => strtotime('+1 day'), + 'scopes' => 'contacts' + ]); + + $data = XeroFacade::getAccessToken(); + + expect($data)->toBe('1234'); +});