Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
farisiskandar7 opened this issue Jan 4, 2021 · 20 comments

Comments

@farisiskandar7
Copy link

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
Copy link
Contributor

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
Copy link
Contributor

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
Copy link
Contributor

gbrits commented Jul 7, 2021

@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
Copy link
Contributor

gbrits commented Jul 7, 2021

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
Copy link
Contributor

gbrits commented Jul 7, 2021

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
Copy link
Contributor

hailwood commented Jul 7, 2021

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

@gbrits
Copy link
Contributor

gbrits commented Jul 8, 2021

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

@hailwood
Copy link
Contributor

hailwood commented Jul 8, 2021

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
Copy link
Contributor

gbrits commented Jul 8, 2021

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
Copy link
Contributor

hailwood commented Jul 8, 2021

I believe so yeah :)

@mohamm6d
Copy link

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

@wijaksanapanji
Copy link

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
Copy link
Contributor

@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
Copy link

wijaksanapanji commented Nov 25, 2021

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
Copy link
Contributor

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
Copy link

@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
Copy link
Contributor

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
Copy link

deanzod commented Sep 30, 2022

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
Copy link

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
Copy link

tschope commented Mar 27, 2024

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

10 participants