[Unreleased]
[1.2.0] - 2026-10-03
Security
BrokerClient::isTrustedRenewUrl()(andOAuthManager::isTrustedRenewUrl(), which wraps it) now rejects licenserenew_urlvalues that PHP'sparse_url()and browsers parse differently (#25). URLs containing backslashes, whitespace or control characters, URLsparse_url()can't parse, and URLs with userinfo are refused before the scheme and broker-host checks. Previously, for an HTTPS broker atworkshop.test, a forgedhttps://evil.test\@workshop.test/renewwas trusted because PHP read its host asworkshop.test, while a browser sends it toevil.test. Only affects broker mode.
Documentation
- The
BrokerClientandOAuthManagerAPI references and Broker Mode list the full set ofrenew_urltrust rules.
[1.1.0] - 2026-10-02
Added
- Stateless relay primitives for OAuth brokers (#22).
AppleOAuth::client( ?AppleCredentials )returns anAppleClientbuilt from runtime credentials (Services ID, Team ID, Key ID, inline.p8PEM, redirect URI) or the configured driver, and never touches the session or database.authorizationUrl()takes the caller'sstate, optionalnonce, scopes and extra parameters, and keepsresponse_mode=form_post.exchangeCode()takes the expected nonce (or none) and the rawuserform field, validates the id_token claims, and returns aTokenResponse.refresh()takes a raw refresh-token string and hands the same token back, with noAppleConnectionrequired.AppleClient::verifyState()checks the caller's state. ClientSecretGenerator::generateFor()/forgetFor()mint and cache the ES256client_secretfor runtimeAppleCredentials.TokenResponsegainsexpiresIn,scopes,withUserId(),fromApple(),fromBroker()andtoArray()(the broker's wire shape).AppleUserProfilegainsdisplayNameandfullName().- Broker client mode (
APPLE_OAUTH_MODE=broker). Connect, callback and refresh run through an OAuth broker using onlyapple-oauth.broker.url,site_id,site_secretandreturn_url, so the site holds no Apple client secret or.p8key. Signed/authorizelinks, one-time code exchange at/token(the id_tokensubbecomes the stable user ID), and refreshes at/refresh. Broker credentials can come from theap.apple-oauth.broker.credentialsfilter.AppleOAuth::broker()andAppleOAuth::usesBroker()expose the client and mode. The broker URL must be HTTPS (plain HTTP only forlocalhost,*.localhost,*.testand loopback hosts). LicenseExpiredException(extendsTokenRefreshException) for the broker's402 license_expiredrefresh response, carryinggetRenewUrl(). The connection stays connected, unlike a revoked grant.OAuthManager::isTrustedRenewUrl()checks arenew_urlfrom the broker's return before it is shown.OAuthExceptionandTokenRefreshExceptionexpose the OAuth error code viagetError().
Changed
OAuthManager::handleCallback()andTokenManager::refresh()are now thin wrappers over the stateless primitives. Behavior in the defaultdirectmode is unchanged.TokenResponse::$userIdandTokenResponse::$profileare now nullable, for stateless responses and for refreshes without an id_token. Session-flow callbacks always set both.TokenManager::store()throws whenuserIdis null.- The
client_secretcache key now includes a SHA-256 fingerprint of the private key, so a corrected or rotated key under the same Key ID never reuses a JWT signed with the old key. Existing cached JWTs are re-minted once after upgrading. TokenManager::store()andTokenManager::refresh()now persist grantedscopeswhen the response reports them (broker responses do; Apple's token endpoint does not).
Documentation
- New pages: Broker Mode, Stateless Client, and API references for
AppleClientandBrokerClient. - Updated the configuration, environment-variable, OAuth, callback, token, exception, client-secret, connection-model, testing, and FAQ docs plus the README for broker mode, the stateless primitives,
getError()/LicenseExpiredException, and the newTokenResponse/AppleUserProfilemembers.
[1.0.0] - 2026-09-18
Added
- Sign in with Apple OAuth2 authorization-code flow, including
id_tokenclaim validation and enforced HTTPS on the token endpoint. - Apple
client_secretminted as an ES256 JWT with rotation support and cached generation. - Encrypted token storage with automatic refresh when access tokens expire.
- Scope registry with the
ap.apple-oauth.scopesfilter hook so packages can contribute and reorder requested scopes. - Configuration repository with config-file and database drivers for storing Services ID, Team ID, Key ID, and
.p8private key material; database driver writes are atomic to close concurrent-write races. - Optional CMS Settings bridge that lets the database driver read/write credentials through the
artisanpack-ui/cms-frameworksettings surface. TokenProviderseam andAppleOAuthManager::request()consumer API for making authenticated calls to Apple services on behalf of a stored connection.AppleOAuthfacade and helper functions covering the public surface.- Comprehensive Pest test suite covering the OAuth callback, token refresh, and client-secret cache paths.
Documentation
- README rewrite covering the OAuth flow, credential drivers, client-secret JWT, scope registry, and consumer API.
- Full
docs/tree: getting started, installation, OAuth, drivers, tokens, scopes, client secret, connection model, API reference, testing, contributing, and FAQ. - Apple Developer setup walkthrough (Services ID,
.p8key, Team ID, Key ID, return URLs).

