# Use PKCE for an Apple Data Connection Flow

Canonical: https://figment.so/blog/apple-connect-oauth-pkce

PKCE — Proof Key for Code Exchange — ties each authorization code to the specific client request that produced it. Without it, an intercepted code can be exchanged by an attacker, not your app. Add PKCE to every Figment Apple Connect flow: [RFC 9700](https://www.rfc-editor.org/rfc/rfc9700) makes it mandatory for all public OAuth clients, and S256 is the only safe hashing method.

## Why PKCE closes the authorization-code injection gap

A public client — a single-page app, a native desktop app, any client that cannot keep a client secret — cannot prove its identity to the token endpoint using a secret. When the authorization server redirects back to your URI with the authorization code, there are scenarios where a malicious app sharing a URI scheme or redirect pattern can intercept that code. With the code in hand, the attacker calls the token endpoint and receives a valid access token for your user's data.

PKCE removes that opportunity. Before the authorization redirect, your client generates a one-time random string called the **code verifier**. It sends a hashed derivative — the **code challenge** — with the authorization request, keeping the original verifier private. The token endpoint requires the original verifier at exchange time, derives the hash itself, and compares. Only the client that started the flow holds the verifier.

RFC 9700 states that public clients **must** use PKCE ([RFC 9700](https://www.rfc-editor.org/rfc/rfc9700)). Use **S256** exclusively. RFC 7636 is explicit: the `plain` method offers no protection against an observer who can read the authorization request, and exists only for compatibility with constrained environments that cannot support S256 ([RFC 7636](https://www.rfc-editor.org/rfc/rfc7636)). A client that falls back to `plain` after attempting S256 should treat that as a potential downgrade attack, not a compatibility accommodation.

Figment Apple Connect issues each app a scoped grant. PKCE protects the authorization code for that grant. Revoking your app's grant stops that app's access and leaves other connected apps unaffected.

## Generate a code verifier and challenge

RFC 7636 specifies the verifier as 43–128 characters from the unreserved set `[A-Za-z0-9\-._~]`, with at least 256 bits of entropy. Encode 32 cryptographically random bytes as base64url — no padding — and you get exactly 43 characters satisfying that requirement.

```python
# Unexecuted example — test output before production use
import secrets, hashlib, base64

# 32 bytes = 256 bits; base64url without padding = 43 characters
raw = secrets.token_bytes(32)
code_verifier = base64.urlsafe_b64encode(raw).rstrip(b"=").decode()

# S256 challenge per RFC 7636 §4.2: BASE64URL(SHA256(ASCII(code_verifier)))
digest = hashlib.sha256(code_verifier.encode("ascii")).digest()
code_challenge = base64.urlsafe_b64encode(digest).rstrip(b"=").decode()
```

Generate a fresh pair for each authorization attempt. Discard the verifier after a successful exchange — retaining it longer than necessary increases exposure without benefit.

## Build the authorization request

Redirect the user's browser to `https://figment.so/apple-connect/authorize` with the following parameters. Register your app and obtain a `client_id` at [/apple-connect/developers](/apple-connect/developers).

| Parameter | Value | Note |
|---|---|---|
| `response_type` | `code` | Required for authorization-code flow |
| `client_id` | your app's client ID | Assigned during app registration |
| `redirect_uri` | exact registered HTTPS URI | RFC 9700: exact string match, no wildcards |
| `scope` | space-separated list | Available scopes: `messages:read`, `messages:send`, `contacts:read`, `calendar:read`, `reminders:read`; request only what your app needs |
| `state` | random, session-bound token | Binds the callback to the initiating session |
| `code_challenge` | base64url(SHA-256(verifier)) | Computed above |
| `code_challenge_method` | `S256` | Never `plain`; always include this parameter explicitly |

**Always include `code_challenge_method` explicitly.** RFC 7636 specifies that if the parameter is omitted, the server defaults to `plain`. Sending `S256` explicitly prevents an unintentional downgrade regardless of server defaults.

**Redirect URI matching is strict.** RFC 9700 requires exact string matching between the URI registered at app setup and the URI sent in both the authorization request and the token exchange. Register the exact URI — including scheme, host, path, and any fixed query string — and do not rely on prefix or wildcard rules. (RFC 9700 provides one narrow exception: variable port numbers for `localhost` redirect URIs in native apps, following RFC 8252.)

**Persist `state` before you redirect.** Store the value in the user's session. Reject any callback where the returned `state` does not match the stored value. This binds the callback to the session that initiated the flow and rules out cross-site request forgery.

## Exchange the code

The authorization server redirects to your registered URI with a `code` parameter. Post that code to the token endpoint along with the original verifier and the same `redirect_uri`:

```http
# Token exchange — unexecuted example; replace placeholders before use
POST https://figment.so/api/apple-connect/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=AUTHORIZATION_CODE
&redirect_uri=YOUR_REGISTERED_REDIRECT_URI
&client_id=YOUR_CLIENT_ID
&code_verifier=CODE_VERIFIER
```

A successful response includes:

```json
{
  "access_token": "ACCESS_TOKEN",
  "refresh_token": "REFRESH_TOKEN",
  "token_type": "Bearer",
  "expires_in": 900,
  "scope": "messages:read",
  "connection_id": "con_..."
}
```

The server derives `BASE64URL(SHA256(verifier))`, compares it to the challenge stored with the code, and issues a token only on a match. A verifier mismatch returns an error. Attempting a second exchange for the same code returns `invalid_grant` — authorization codes are single-use.

**Treat an unknown outcome as an error.** If the token exchange call times out or returns an ambiguous response, do not silently retry. You cannot determine from the client whether the code was consumed. Restart the full authorization flow rather than reusing the code.

## Manage access and refresh tokens

Access tokens expire after 900 seconds. Refresh tokens are separate rotating credentials. After every refresh call, store the new refresh token and serialize refresh operations per grant to avoid concurrent invalidation. Refresh using `grant_type=refresh_token` with your `client_id` and current `refresh_token`. Figment tokens are opaque strings — do not parse them as JWTs.

## Apple account authorization and connection states

PKCE protects your app's authorization code. The Apple account sign-in itself — trusted-device confirmation, Mac permission prompts, iCloud sync — happens inside Figment's hosted connection setup, separate from your app's OAuth flow. The developer's app never receives or stores the Apple account password.

Apple supports third-party authorization for Mail, Calendar, and Contacts through approved apps. Users can review and revoke individual grants at [account.apple.com](https://account.apple.com) under Sign-In and Security > Account Data Sharing ([Apple support](https://support.apple.com/en-us/121539)). Figment's app-consent grant is distinct from that Apple-issued grant; Figment is the issuer of the app-access token and connection record.

After a successful grant, Figment tracks connection state per service. A completed OAuth exchange does not mean data is immediately available. Check the connection before making data requests:

```http
GET https://figment.so/api/apple-connect/v1/connections/con_.../
Authorization: Bearer ACCESS_TOKEN
```

The response uses the form `{connection:{id, mode, services, ...}}`. Each service in `connection.services` is an object with `state`, `reason`, and `observedAt` (epoch milliseconds); only granted services appear. Possible states:

| State | What it means |
|---|---|
| `connecting` | Initial handshake in progress |
| `needs_approval` | A Mac permission or trusted-device step is pending |
| `syncing` | Data indexing in progress |
| `ready` | Service is responding to requests |
| `offline` | Service is unreachable |
| `reconnect_required` | Grant needs re-establishment |
| `revoked` | Access has been revoked |

The endpoint enforces authorization and can return a non-2xx response for revoked or non-ready connections rather than a diagnostic state object — handle non-2xx responses in your code. A `ready` state does not guarantee complete history or successful delivery. Use [/apple-connect/account](/apple-connect/account) to view and manage setup status.

## Authorization-code sequence and state/PKCE test worksheet

Use this worksheet before shipping. Each row maps a flow event to the parameter it produces and the assertion your test should make.

| Step | Action | Parameter or check | Assertion |
|---|---|---|---|
| 1 | Generate verifier | `code_verifier` | Length 43–128; characters in `[A-Za-z0-9\-._~]` only |
| 2 | Derive S256 challenge | `code_challenge` | Equals `BASE64URL(SHA256(verifier))`; no `=` padding |
| 3 | Authorization request | `code_challenge_method` | Value is `S256`; parameter is present; server must reject `plain` |
| 4 | Authorization request | `state` | Cryptographically random; stored in session before redirect |
| 5 | Callback received | `state` check | Reject if returned value does not match stored session value |
| 6 | Token exchange | `code_verifier` | Identical string to step 1; any altered value returns an error |
| 7 | Code reuse | second exchange attempt | Server returns `invalid_grant` on the second request |
| 8 | Redirect URI consistency | `redirect_uri` | Byte-for-byte identical in authorization request and token request |
| 9 | Connection state | `GET /v1/connections/{connection_id}` | `connection.services.[service].state` is `ready` before data operations proceed |
| 10 | Revocation | Apple Account Data Sharing revoke | Service state changes; check [/apple-connect/account](/apple-connect/account) for current connection status |

Steps 6 and 7 together confirm that the server treats the verifier as a one-time proof and the code as single-use. If either assertion fails in staging, the authorization server configuration needs review before connecting real user accounts.

---

Register your app and start the connection flow at [**Connect Apple Account**](/apple-connect/developers). For the full consent and scoping model: [OAuth for iCloud: Connect Apple Data to Your App](/oauth-for-icloud). For framework-specific setup: [Add Connect Apple Account to a Next.js App](/blog/apple-connect-nextjs).
