Skip to content

ASA: switch OAuth token exchange to query-param + client_secret shape#6

Merged
bootuz merged 1 commit into
mainfrom
feat/asa-oauth-client-secret
May 23, 2026
Merged

ASA: switch OAuth token exchange to query-param + client_secret shape#6
bootuz merged 1 commit into
mainfrom
feat/asa-oauth-client-secret

Conversation

@bootuz
Copy link
Copy Markdown
Owner

@bootuz bootuz commented May 23, 2026

What

Update ASATokenCache.exchange in Sources/App/Clients/AppleSearchAdsClient.swift so the OAuth token-exchange request matches Apple's currently-documented Search Ads shape.

Before After
Params location POST body (form-urlencoded) URL query string
Auth param client_assertion + client_assertion_type=urn:…:jwt-bearer client_secret
Body the form fields empty
X-AP-Context on OAuth call added when orgId present removed (data calls still send it)

Reference (Apple's docs, dummy values):

POST https://appleid.apple.com/auth/oauth2/token
  ?grant_type=client_credentials
  &client_id=SEARCHADS.<uuid>
  &client_secret=<jwt>
  &scope=searchadsorg
Content-Type: application/x-www-form-urlencoded
(empty body)

Why

Apple shifted the public Search Ads OAuth surface: the JWT minted from the user's EC private key is now passed as client_secret rather than as an RFC 7521 JWT-bearer client_assertion. Credentials freshly generated from Apple's console were failing against the old wire shape with invalid_client. This makes our exchange byte-for-byte identical to the docs' reference curl.

No schema or settings-UI change — ASACredentials.clientSecret already holds the JWT; only the wire field name changes. Downstream campaign/report calls keep X-AP-Context via authHeaders(token:)X-AP-Context is only removed from the OAuth hop, where Apple's docs don't include it.

Reviewer notes

  • The new test tokenCache_oauthRequestShape asserts the OAuth URL contains all four query params and that X-AP-Context is absent on the exchange call. It also confirms that percent(_:) leaves RFC 3986 unreserved chars (-._~) literal, so the JWT's dots aren't double-encoded.
  • The Exchanged / OAuthResponse model and the 401-retry / cache-invalidate paths are unchanged.
  • If you've previously seen ASA orgs that genuinely required X-AP-Context on the auth hop, flag it — easy to add back, but it's not in any current Apple doc I could find.

Verification

  • swift build — clean
  • swift test --filter AppleSearchAdsClientTests — 7/7 pass (6 existing + 1 new)

Apple's current Search Ads OAuth docs use URL query parameters with an
empty body and pass the JWT as 'client_secret' rather than as a
'client_assertion' JWT-bearer assertion. Update ASATokenCache.exchange
to match the documented shape so credentials minted from Apple's console
work without massaging.

- Move grant_type / client_id / client_secret / scope onto the URL query
- Drop client_assertion_type, send an empty body
- Stop sending X-AP-Context on the OAuth call (data calls still get it)
- Refresh leading comment block to describe the new flow
- Add a test asserting the OAuth URL contains all four query params and
  that X-AP-Context is absent on the exchange request
@bootuz bootuz merged commit c83393b into main May 23, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant