Skip to content

Authentication

How to create a Twinfield client in your own application, without the example helper.

Twinfield uses OAuth 2.0. A user logs in once (authorization code flow) and your application stores the refresh token. With that refresh token, RefreshTokenAccessTokenProvider gets access tokens and refreshes them when they expire (after about an hour). It also finds the cluster URL of the organisation for you.

use SolveBeam\Twinfield\OAuth2\Provider\Twinfield as TwinfieldProvider;
use SolveBeam\Twinfield\OAuth2\RefreshTokenAccessTokenProvider;
use SolveBeam\Twinfield\Twinfield;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
$provider = new TwinfieldProvider([
'clientId' => 'your-client-id',
'clientSecret' => 'your-client-secret',
'redirectUri' => 'https://example.com/twinfield/callback',
]);
$tokens = new RefreshTokenAccessTokenProvider(
$provider,
refreshToken: $storedRefreshToken,
cache: new FilesystemAdapter('twinfield'), // any PSR-6 cache
);
$twinfield = Twinfield::create($tokens)->company('1000');

Twinfield::create() finds an installed PSR-18 HTTP client and PSR-17 factories automatically. You can also pass your own:

$twinfield = Twinfield::create($tokens, $httpClient, $requestFactory, $streamFactory);

A user logs in once with the authorization code flow, and you store the refresh token you get back. See Login for a complete example.

Request at least these scopes: openid, twf.organisationUser and offline_access (needed for a refresh token). See OpenID Connect scopes.

Twinfield currently returns the same refresh token on every refresh. If it ever returns a new one, RefreshTokenAccessTokenProvider uses it from then on. Store it so you have it on the next request:

if ($tokens->refreshToken() !== $storedRefreshToken) {
$storedRefreshToken = $tokens->refreshToken(); // save it
}

A refresh token is valid for about 25 years, but it can stop working earlier. The first API call then throws an IdentityProviderException. Ask the user to log in again:

use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
try {
$offices = $twinfield->offices()->list();
} catch (IdentityProviderException $exception) {
$error = $exception->getResponseBody()['error'] ?? null;
// Send the user to the login again.
}
Error Cause
invalid_grant Wrong client ID or secret; refresh token invalid, expired or issued to another client; client may not use offline_access or a requested scope
invalid_request Refresh token missing; organisation inactive or deleted; user deleted, disabled, locked or password expired
unsupported_grant_type Grant type missing or invalid (a bug, not something the user can fix)
Terminal window
export TWINFIELD_CLIENT_ID=… TWINFIELD_CLIENT_SECRET=… TWINFIELD_REDIRECT_URI=…
export TWINFIELD_REFRESH_TOKEN=… TWINFIELD_COMPANY=…
php examples/getting-started/authentication/example.php

After composer twinfield:setup you find the refresh token in ~/.config/twinfield/token.json.

Connected to https://api.accounting.twinfield.com
Company 1000 has access to 3 offices.

The other examples start with $twinfield = Example::twinfield();. That helper does the same as this example, with the values from ~/.config/twinfield/. The first time, it asks for your client details and opens the login in your browser.

View examples/getting-started/authentication/example.php on GitHub.