Since 1.1.0.
ArtisanPackUI\AppleOAuth\Broker\BrokerClient runs the connect, exchange, and refresh steps through an OAuth broker. It implements the site-facing side of the broker contract: signed /authorize links, the one-time code exchange at /token, and refreshes at /refresh. Prose walkthrough: Broker Mode.
Constants
| Constant | Value | Purpose |
|---|---|---|
PROVIDER |
apple |
Provider segment in broker URLs and the signature payload. |
LINK_TTL_SECONDS |
300 |
How long a signed /authorize link stays valid. The broker accepts at most 10 minutes. |
Constructor
public function __construct(
protected BrokerCredentials $credentials,
protected HttpFactory $http,
)
Usually obtained through AppleOAuth::broker() or OAuthManager::brokerClient().
Static methods
isEnabled( ConfigRepository $config ): bool
true when apple-oauth.mode is broker.
fromConfig( ConfigRepository $config, HttpFactory $http ): self
Builds a client from BrokerCredentials::fromConfig(). Throws OAuthException("Apple OAuth broker credentials are not configured.") when the URL, site ID, or site secret is missing.
Methods
credentials(): BrokerCredentials
The credentials the client authenticates with.
authorizationUrl()
public function authorizationUrl( string $state, string $returnUrl, ?array $scopes = null ): string
The signed {url}/api/v1/oauth/apple/authorize link. Query: site_id, state, return_url, expires (now + 300s), optional scopes (space-separated; omitted when null or empty, which asks for every scope the broker allows), and signature.
signature( array $params ): string
HMAC-SHA256 of "apple\n" . http_build_query( ksort( $params ), RFC 3986 ), keyed with BrokerCredentials::signingKey(). Any signature key in $params is ignored.
exchangeCode( string $code ): TokenResponse
POSTs grant_type=authorization_code&code=… to {url}/api/v1/oauth/token with the site secret as the bearer token. Returns TokenResponse::fromBroker().
Throws OAuthException when the broker returns an error (getError() and getRenewUrl() are set), returns no access_token (exchange_failed), or returns no id_token with a sub claim (exchange_failed).
refresh( string $refreshToken, string $tokenType = 'Bearer' ): TokenResponse
POSTs refresh_token=…&provider=apple to {url}/api/v1/oauth/refresh. The returned response keeps $refreshToken, since the broker sends none back.
| Broker result | Throws |
|---|---|
HTTP 402, or error=license_expired |
LicenseExpiredException, with getRenewUrl() |
| Any other error | TokenRefreshException. getError() is the broker's code, e.g. invalid_grant. |
2xx without access_token |
TokenRefreshException (refresh_failed) |
isTrustedRenewUrl( ?string $url ): bool
Whether a renew_url is safe to link to. Use it before showing a renew_url that arrived on a query string. Returns true only when the URL:
- has no backslashes, whitespace, control characters, or userinfo (
user:pass@), andparse_url()can parse it, - is on the broker's own host, and
- uses HTTPS, or HTTP when the broker itself is plain HTTP. A downgrade from an HTTPS broker is never accepted.
The first rule (since 1.2.0) rejects URLs that PHP and browsers parse differently. For an HTTPS broker at workshop.test, PHP reads the host of https://evil.test\@workshop.test/renew as workshop.test, but a browser sends it to evil.test.
OAuthManager::isTrustedRenewUrl() wraps this and also returns false outside broker mode.
BrokerCredentials
ArtisanPackUI\AppleOAuth\Broker\BrokerCredentials holds the immutable credentials a site uses to talk to the broker.
final class BrokerCredentials
{
public function __construct(
public readonly string $url, // no trailing slash
public readonly string $siteId,
public readonly string $siteSecret, // "{id}|{plain}"
) {}
public static function isSecureUrl( string $url ): bool;
public static function fromConfig( ConfigRepository $config ): ?self;
public function signingKey(): string;
}
- Constructor. Throws
OAuthExceptionwhen$urlisn't secure (seeisSecureUrl()). isSecureUrl(). Returnstruefor HTTPS URLs. Returnstruefor HTTP only onlocalhost,*.localhost,*.test,127.x.x.x, and::1.fromConfig(). Readsapple-oauth.broker.{url,site_id,site_secret}, passes them through theap.apple-oauth.broker.credentialsfilter, then trims them (and any trailing/on the URL). Returnsnullwhen any value is empty.signingKey(). Returns the SHA-256 of the plain part of the site secret (after the|). A secret without|is treated as entirely plain.

