Aller au contenu

OAuth

Pennylane\Sdk\OAuth\OAuthApp implements the OAuth 2.0 authorization code flow for multi company partner integrations.

  1. Send the user to authorizationUrl().
  2. Exchange the received code with exchangeCode().
  3. Use new Pennylane(apiToken: $tokens->accessToken); access tokens expire after 24 hours.
  4. Refresh with refresh().

Pennylane applies refresh token rotation: every refresh invalidates BOTH previous tokens. Never run two refreshes concurrently, including across processes, and persist the returned pair immediately after every refresh.

OAuthApp

public function __construct(
    public readonly string $clientId,
    string $clientSecret,
    public readonly string $redirectUri,
    string $authorizeUrl = self::DEFAULT_AUTHORIZE_URL,
    string $tokenUrl = self::DEFAULT_TOKEN_URL,
    ?ClientInterface $httpClient = null,
    ?RequestFactoryInterface $requestFactory = null,
    ?StreamFactoryInterface $streamFactory = null,
)

clientSecret is marked #[\SensitiveParameter] and kept private; it is never exposed as a public property. authorizeUrl and tokenUrl default to OAuthApp::DEFAULT_AUTHORIZE_URL (https://app.pennylane.com/oauth/authorize) and OAuthApp::DEFAULT_TOKEN_URL (https://app.pennylane.com/oauth/oauth/token), and are validated with the same https rule as the main clients (http allowed on localhost only). httpClient, requestFactory and streamFactory follow the same discovery rules as Pennylane when omitted.

Methods

Method Description
authorizationUrl(array\|string $scopes, ?string $state = null): string Builds the URL where the user grants your app access. $scopes is a list of scope strings (for example ['customer_invoices:all']) or an already space separated string. $state is an opaque anti CSRF value returned to your redirect URI.
exchangeCode(string $code): OAuthTokens Exchanges the authorization code for the initial token pair.
refresh(string $refreshToken): OAuthTokens Gets a new token pair. Persist the returned pair immediately: because of refresh token rotation, the pair you passed in is now invalid. $refreshToken is marked #[\SensitiveParameter].

Both exchangeCode() and refresh() throw ApiConnectionException / ApiTimeoutException on a transport failure, an ApiStatusException subclass when the token endpoint answers with an error status, and InvalidResponseException when the response is not JSON or is missing access_token.

OAuthTokens

Pennylane\Sdk\OAuth\OAuthTokens extends PennylaneObject. Both token values are redacted from var_dump() output (through __debugInfo()); access them explicitly through the properties below.

Property Type Description
accessToken string The access token to pass as apiToken to Pennylane or PennylaneFirm. #[\SensitiveParameter].
refreshToken ?string The refresh token, if the response included one. #[\SensitiveParameter].
tokenType ?string Token type, typically "bearer".
expiresIn ?int Seconds until accessToken expires.
scope ?string Space separated granted scopes.
createdAt ?int Unix timestamp of token creation.

Example

<?php

declare(strict_types=1);

require __DIR__ . '/vendor/autoload.php';

use Pennylane\Sdk\OAuth\OAuthApp;
use Pennylane\Sdk\Pennylane;

$app = new OAuthApp(
    clientId: getenv('PENNYLANE_OAUTH_CLIENT_ID'),
    clientSecret: getenv('PENNYLANE_OAUTH_CLIENT_SECRET'),
    redirectUri: 'https://example.com/oauth/callback',
);

// Step 1: redirect the user to this URL.
$url = $app->authorizationUrl(['customer_invoices:all', 'suppliers:readonly'], state: bin2hex(random_bytes(16)));

// Step 2: in your callback route, exchange the code query parameter.
$tokens = $app->exchangeCode($_GET['code']);
// Persist $tokens->accessToken, $tokens->refreshToken and $tokens->expiresIn.

// Step 3: use the access token as any other Pennylane client.
$client = new Pennylane(apiToken: $tokens->accessToken);

// Step 4: refresh before expiry, and persist the new pair immediately.
$refreshed = $app->refresh($tokens->refreshToken);