Closed farisiskandar7 closed 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
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);
}
}
@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?
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.
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.
Thanks for the feedback @gbrits, I wrote than on github without testing so I'm not surprised there's some errors 😅
Not even errors, just one typo, pretty amazing for untested! You sir, are a gentleman and a scholar 🤓
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 :)
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;
?
I believe so yeah :)
Change & update line 26:
$this->use
to
$this->user
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?
@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
.
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
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.
@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?
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.
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?
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!
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
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,