webfox / laravel-xero-oauth2

A Laravel integration for Xero using the Oauth 2.0 spec
MIT License
50 stars 32 forks source link

I have stored CLIENT ID and CLIENT SECRET in database. Can we connect it based from the database? #45

Closed farisiskandar7 closed 3 years ago

farisiskandar7 commented 3 years ago

Currently, we retrieve the CLIENT ID and CLIENT SECRET from env. This means can I make this package dynamically. I have stored CLIENT ID and CLIENT SECRET in my database so it will connect per user. For a different user, they will have a different CLIENT ID and CLIENT SECRET.

Thank you,

bumperbox commented 3 years ago

The client id and secret identify the application not the user, in the apps i have built, the the xero auth token is stored per user

hailwood commented 3 years ago

Hi @farisiskandar7,

@bumperbox is correct, you should only need to store a single set of client credentails as they specify how your application talks to xero, they don't authenticate the user.

For more information about the credential storage which identifies the user see here https://github.com/webfox/laravel-xero-oauth2#credential-storage

An example UserStorageProvider (assuming you have a nullable json column called xero_oauth on your user table and the appropriate casts setup on the model) could look like this


<?php

namespace App\Xero;

use App\Models\User;
use Illuminate\Session\Store;
use League\OAuth2\Client\Token\AccessTokenInterface;
use Webfox\Xero\Oauth2Provider;
use Webfox\Xero\OauthCredentialManager;

class UserStorageProvider implements OauthCredentialManager
{

    /** @var Oauth2Provider  */
    protected $oauthProvider;

    /** @var Store */
    protected $session;

   /** @var User */
    protected $user;

    public function __construct(User $user, Store $session, Oauth2Provider $oauthProvider)
    {
        $this->use           = $user;
        $this->oauthProvider = $oauthProvider;
        $this->session       = $session;
    }

    public function getAccessToken(): string
    {
        return $this->data('token');
    }

    public function getRefreshToken(): string
    {
        return $this->data('refresh_token');
    }

    public function getTenantId(): string
    {
        return $this->data('tenant_id');
    }

    public function getExpires(): int
    {
        return $this->data('expires');
    }

    public function getState(): string
    {
        return $this->session->get('xero_oauth2_state');
    }

    public function getAuthorizationUrl(): string
    {
        $redirectUrl = $this->oauthProvider->getAuthorizationUrl(['scope' => config('xero.oauth.scopes')]);
        $this->session->put('xero_oauth2_state', $this->oauthProvider->getState());

        return $redirectUrl;
    }

    public function getData(): array
    {
        return $this->data();
    }

    public function exists(): bool
    {
        return !!$this->user->xero_oauth;
    }

    public function isExpired(): bool
    {
        return time() >= $this->data('expires');
    }

    public function refresh(): void
    {
        $newAccessToken = $this->oauthProvider->getAccessToken('refresh_token', [
            'refresh_token' => $this->getRefreshToken(),
        ]);

        $this->store($newAccessToken);
    }

    public function store(AccessTokenInterface $token, string $tenantId = null): void
    {
        $this->user->xero_oauth = [
            'token'         => $token->getToken(),
            'refresh_token' => $token->getRefreshToken(),
            'id_token'      => $token->getValues()['id_token'],
            'expires'       => $token->getExpires(),
            'tenant_id'     => $tenantId ?? $this->getTenantId()
        ];

        $this->user->saveOrFail();
    }

    public function delete(): void
    {
        $this->user->xero_oauth = null;
        $this->user->saveOrFail();
    }

    public function getUser(): ?array
    {

        try {
            $jwt = new \XeroAPI\XeroPHP\JWTClaims();
            $jwt->setTokenId($this->data('id_token'));
            $decodedToken = $jwt->decode();

            return [
                'given_name'  => $decodedToken->getGivenName(),
                'family_name' => $decodedToken->getFamilyName(),
                'email'       => $decodedToken->getEmail(),
                'user_id'     => $decodedToken->getXeroUserId(),
                'username'    => $decodedToken->getPreferredUsername(),
                'session_id'  => $decodedToken->getGlobalSessionId()
            ];
        } catch (\Throwable $e) {
            return null;
        }
    }

    protected function data($key = null)
    {
        if (!$this->exists()) {
            throw new \Exception('Xero oauth credentials are missing');
        }

        $cacheData = $this->user->xero_oauth;

        return empty($key) ? $cacheData : ($cacheData[$key] ?? null);
    }
}
gbrits commented 3 years ago

@hailwood - I may be an idiot, but I have tried to implement this unsuccessfully. I have done everything as instructed and I'm referencing this created file that I have put in app/Xero/UserStorageProvider.php and I fixed the typo in your constructor ($this->use). When I click to connect to Xero, it goes through to Xero, I connect and it redirects back to my app, there's a bunch of GET variables in the return URL but this class doesn't appear to interact with the feedback at all... is there a step I'm missing?

gbrits commented 3 years ago

I missed this part in the documentation: https://{your-domain}/xero/auth/callback -- I was redirecting back to my success callback each time, so it was skipping the process altogether.

gbrits commented 3 years ago

PS. In the data method, I had to change my $cacheData call to:

$cacheData = json_decode($this->user->fresh()->xero_oauth, true);

to interpret the string correctly. Hope that helps someone.

hailwood commented 3 years ago

Thanks for the feedback @gbrits, I wrote than on github without testing so I'm not surprised there's some errors 😅

gbrits commented 3 years ago

Not even errors, just one typo, pretty amazing for untested! You sir, are a gentleman and a scholar 🤓

hailwood commented 3 years ago

Cheers,

Side note, if you add a json cast to your User model for xero_oauth you won't need to json_decode it manually :)

gbrits commented 3 years ago

Oh cool I have never used casts for anything other than dates before.

So, as in:

protected $casts = [
    'xero_oauth' => 'array'
];

and then I can just do:

$cacheData = $this->user->fresh()->xero_oauth;

?

hailwood commented 3 years ago

I believe so yeah :)

mohamm6d commented 3 years ago

Change & update line 26: $this->use to $this->user

wijaksanapanji commented 3 years ago

Hi @farisiskandar7,

@bumperbox is correct, you should only need to store a single set of client credentails as they specify how your application talks to xero, they don't authenticate the user.

For more information about the credential storage which identifies the user see here https://github.com/webfox/laravel-xero-oauth2#credential-storage

An example UserStorageProvider (assuming you have a nullable json column called xero_oauth on your user table and the appropriate casts setup on the model) could look like this

<?php

namespace App\Xero;

use App\Models\User;
use Illuminate\Session\Store;
use League\OAuth2\Client\Token\AccessTokenInterface;
use Webfox\Xero\Oauth2Provider;
use Webfox\Xero\OauthCredentialManager;

class UserStorageProvider implements OauthCredentialManager
{

    /** @var Oauth2Provider  */
    protected $oauthProvider;

    /** @var Store */
    protected $session;

   /** @var User */
    protected $user;

    public function __construct(User $user, Store $session, Oauth2Provider $oauthProvider)
    {
        $this->use           = $user;
        $this->oauthProvider = $oauthProvider;
        $this->session       = $session;
    }

    public function getAccessToken(): string
    {
        return $this->data('token');
    }

    public function getRefreshToken(): string
    {
        return $this->data('refresh_token');
    }

    public function getTenantId(): string
    {
        return $this->data('tenant_id');
    }

    public function getExpires(): int
    {
        return $this->data('expires');
    }

    public function getState(): string
    {
        return $this->session->get('xero_oauth2_state');
    }

    public function getAuthorizationUrl(): string
    {
        $redirectUrl = $this->oauthProvider->getAuthorizationUrl(['scope' => config('xero.oauth.scopes')]);
        $this->session->put('xero_oauth2_state', $this->oauthProvider->getState());

        return $redirectUrl;
    }

    public function getData(): array
    {
        return $this->data();
    }

    public function exists(): bool
    {
        return !!$this->user->xero_oauth;
    }

    public function isExpired(): bool
    {
        return time() >= $this->data('expires');
    }

    public function refresh(): void
    {
        $newAccessToken = $this->oauthProvider->getAccessToken('refresh_token', [
            'refresh_token' => $this->getRefreshToken(),
        ]);

        $this->store($newAccessToken);
    }

    public function store(AccessTokenInterface $token, string $tenantId = null): void
    {
        $this->user->xero_oauth = [
            'token'         => $token->getToken(),
            'refresh_token' => $token->getRefreshToken(),
            'id_token'      => $token->getValues()['id_token'],
            'expires'       => $token->getExpires(),
            'tenant_id'     => $tenantId ?? $this->getTenantId()
        ];

        $this->user->saveOrFail();
    }

    public function delete(): void
    {
        $this->user->xero_oauth = null;
        $this->user->saveOrFail();
    }

    public function getUser(): ?array
    {

        try {
            $jwt = new \XeroAPI\XeroPHP\JWTClaims();
            $jwt->setTokenId($this->data('id_token'));
            $decodedToken = $jwt->decode();

            return [
                'given_name'  => $decodedToken->getGivenName(),
                'family_name' => $decodedToken->getFamilyName(),
                'email'       => $decodedToken->getEmail(),
                'user_id'     => $decodedToken->getXeroUserId(),
                'username'    => $decodedToken->getPreferredUsername(),
                'session_id'  => $decodedToken->getGlobalSessionId()
            ];
        } catch (\Throwable $e) {
            return null;
        }
    }

    protected function data($key = null)
    {
        if (!$this->exists()) {
            throw new \Exception('Xero oauth credentials are missing');
        }

        $cacheData = $this->user->xero_oauth;

        return empty($key) ? $cacheData : ($cacheData[$key] ?? null);
    }
}

I'm trying to implement something like this, but I'm not using User Model, I use another Model to save the credentials, is there an example where the model is passed to this custom Store?

hailwood commented 3 years ago

@wijaksanapanji just swap out passing in a User model for your custom model, and then replace all instances of $this->user or $user or $model.

wijaksanapanji commented 3 years ago

swap out passing in a User model for your custom mode

Do I need to rebind in AppServiceProvier? Because the model I'm using needed to be created first

hailwood commented 3 years ago

Yes. You'll need to rebind it so you can pass in whatever parameters your custom credential store needs to be instantiated. Remember that this is just an example, you'll want to customize it to fit your application.

samjarmakani commented 2 years ago

@hailwood I'm using your StorageProvider example for a "Website" class I'm using since my site uses Tenancy (https://tenancy.dev/) for Laravel. In my AppServiceProvider I'm passing through the current website using the $this->app->bind(OauthCredentialManager::class, function(Application $app).

Everything seems to work fine until I add an organization. Instead of using the current Website (which is retrieved in the AppServiceProvider) it creates a new record in my websites table and inserts the xero_oauth there. Would you happen to know why this is happening?

hailwood commented 2 years ago

Hi @SamKani92,

If I had to guess I'd say that rather than resolving the current Website, it's resolving an empty model which then gets created in the database when we save the xero details.

deanzod commented 2 years ago

I've been trying to use this provider class and can't get it to fully work. It is storing the token json in the db on the user ok but throws an error after clicking 'allow' undefined array key "tenants".

The class wouldn't work at all unless I added getTenants() to match the implemented OauthCredentialManager:

    public function getTenants(): ?array
    {
        return $this->data('tenants');
    }

Also I had to change the $tenantId to array type on the store method.

Any ideas where I might be going wrong?

BaronSAID commented 8 months ago

Hi everyone! I have a question regarding Laravel-Xero-OAuth2. Is it possible to set the credentials "XERO_CLIENT_ID" and "XERO_CLIENT_SECRET" to be fetched from the user table in the database after authentication? I'm looking forward to your suggestions and solutions. Thanks in advance!

tschope commented 8 months ago

Hi everyone! I have a question regarding Laravel-Xero-OAuth2. Is it possible to set the credentials "XERO_CLIENT_ID" and "XERO_CLIENT_SECRET" to be fetched from the user table in the database after authentication? I'm looking forward to your suggestions and solutions. Thanks in advance!

You can take a look at the config file and check which are variables available or add which one do you prefer: https://github.com/webfox/laravel-xero-oauth2/blob/master/config/config.php