As posted previously, we have upgraded to OAuth2 recently and its great – the security features alone make it 100% worth the effort, but let me tell you there was definitely some effort. For the most part I used this: https://github.com/XeroAPI/xero-php-oauth2, however I did alot of learning by reading through issues in https://github.com/calcinai/xero-php as there is obviously some overlap. I’ve implemented the prior across my system, however I have tested the calcinai version and it seems fit for purpose. I just didn’t feel like rewriting large sections of code again inside 3 weeks.

This is also me asking for help in the middle of not really understanding who/what/why? https://community.xero.com/developer/discussion/116313093/

I’ve spent weeks on this now, on and off. Sometimes clients are all good, sometimes the system is forcing them to re-authorise. Very frustrating and difficult to debug – because, as you may have guessed, I only have one Xero login.

Anyway, long story short – here is the actual steps that you may be looking for if you want to setup Xero OAuth2 with true Multi-User Multi-Tenant.

A few points to get us started.

A ‘user’ (username/password combination with Xero) has tenants (1 or more).
A ‘user’ can be considered a unique Access Token in Xero’s OAuth2. Even when you are visually looking at them, use copy/paste to confirm they are different.
An Access Token is required for every API request.
Access Token’s expire, and require refreshing every 30 mins (if you have offline_access scope included in your Token Authorisation).

Put this all together and you need to build a system where user’s 5x critical Xero OAuth2 details need to be recorded (in a database) on a per user basis. ie. this means keep them separate, there is no sharing.

However, there is sharing when you are talking about access 1 (ONE) specific user’s various (potential) tenants. Specifically the Refresh Token. So when you are running commands at the API – if all the tenants belong to the user (aka Access Token) & Refresh Token, you are all ok.

If you try to access another tenant with the wrong Access Token – you get a non descript ‘invalid_grant’ or Unauthorised Exception.

Now I spent a large amount of timing assuming it was the Refresh Token process that was causing me the issues, however, tonight I’ve finally figured it all out and it was not specifically to blame. It was mainly my lack of keeping all 5 pieces of data seperate for each user. and making sure I use the correct Refresh Token.

Here is how I handle the general Xero integration at a high level:

  1. Authenticate user locally, open up local slice based on auth.
  2. Check if this slice has xero token:
    $storage = new StorageClass();
  3. Check if this token has expired:
    // flag race condition here.
    if ($storage->getHasExpired()) {
  4. Check race condition (imagine 2 users calling a page at the exact same time and both requesting a new refresh token)
    // make sure someone else hasn't already doing this..
    $check_running = db_select ## check db for existance of race condition
    db_insert() ## do db insert with race condition.
  5. attempt to Refresh Token
    try {
    $provider = new \League\OAuth2\Client\Provider\GenericProvider([
    'clientId' => 'ccc',
    'clientSecret' => 'xxx',
    'redirectUri' => 'vvv',
    'urlAuthorize' => 'https://login.xero.com/identity/connect/authorize',
    'urlAccessToken' => 'https://identity.xero.com/connect/token',
    'urlResourceOwnerDetails' => 'https://api.xero.com/api.xro/2.0/Organisation'
    ]); $newAccessToken = $provider->getAccessToken('refresh_token', [ 'refresh_token' => $storage->getRefreshToken() ]);
  6. Save new token details to session & database // Save my token, expiration and refresh token $storage->setToken( $newAccessToken->getToken(), $newAccessToken->getExpires(), $xeroTenantId, $newAccessToken->getRefreshToken(), $newAccessToken->getValues()["id_token"] ); $xero = true; db_delete ## delete race condition flag from database
  7. Handle exceptions, create xero pieces and move on: } catch (Exception $e) { db_delete ## delete race condition flag from database if($e->getCode() == 403){ echo '<div class="alert alert-danger">OAuth2 connection to Xero has expired or been cancelled. Go fish.</div>'; }else{ //echo 'Exception when calling RefreshToken ', $e->getMessage(), PHP_EOL; //ecco($e); //exit; } } }else{ sleep(2); // chuck a sleep in here - just in case 2 users are going crazy with clicks refreshing this page a million times. } }else{ $xero = true; } $config = XeroAPI\XeroPHP\Configuration::getDefaultConfiguration()->setAccessToken( (string)$storage->getSession()['token'] ); $apiInstance = new XeroAPI\XeroPHP\Api\AccountingApi( new GuzzleHttp\Client(), $config ); }

The use case for my system is generally daily access, certainly weekly at worst so I’m not really concerned with hitting the 60 day complete expiry of a Access Token (or Refresh Token). If you are worried – just setup a cron, loop through your valid Access Tokens (or Refresh Tokens) at the 59 day mark and Refresh them (and save the results).

Anyway, that is enough of a rant for now. Hopefully this helps someone who is struggling with the Xero api like I did…

Actually, one last bit for you: this is a simple test page I made so that I can check what is going on (I have multiple browsers all logged into different users (which in turn have different xero users/tenants).

    $xeroTenantId = (string)$storage->getXeroTenantId();

    try {
        $config = XeroAPI\XeroPHP\Configuration::getDefaultConfiguration()->setAccessToken( (string)$storage->getSession()['token'] );
        $apiInstance = new XeroAPI\XeroPHP\Api\AccountingApi(
          new GuzzleHttp\Client(),
        $identityApi = new XeroAPI\XeroPHP\Api\IdentityApi(
          new GuzzleHttp\Client(),

        $orgDetails = $identityApi->getConnections();
    } catch (Exception $e) {
        if($e->getCode() == 403){
          echo '<div class="alert alert-danger">OAuth2 connection to Xero has expired or been cancelled. Go fish.</div>';
          echo 'Exception when calling RefreshToken ', $e->getMessage(), PHP_EOL;
          if($e->getMessage() == 'invalid_grant'){
          	// most likely xero OAuth2 has expired.
          	redirect();#go to authorisation process

    echo '<div class="alert alert-success">Currently connected to Xero for '.$orgDetails[0]['tenant_name'].'! Session expires on '.date("Y-m-d H:i:s", $storage->getExpires()).' </div>';			}elseif($storage->getSession()){
    echo '<div class="alert alert-warning">Currently authenticated to Xero, however its expired: '.date("Y-m-d H:i:s", $storage->getExpires()).' </div>';


Comments are closed