diff --git a/docs/api-reference.md b/docs/api-reference.md new file mode 100644 index 00000000..cc122efd --- /dev/null +++ b/docs/api-reference.md @@ -0,0 +1,49 @@ +# API Reference + +This section provides a high-level overview of the main classes and protocols within `AppCheckCore`. For detailed API documentation, please refer to the header files directly. + +## Core Classes + +### `GACAppCheck` +The central class for managing App Check tokens. It serves as the primary entry point for your application to interact with the App Check system. + +* **Purpose:** Manages the lifecycle of App Check tokens, including fetching, caching, and refreshing. It delegates attestation logic to an `GACAppCheckProvider` instance. +* **Key Methods:** + * `tokenForcingRefresh:completion:`: Requests an App Check token, with an option to force a refresh, bypassing the cache. + * `limitedUseTokenWithCompletion:`: Requests a limited-use App Check token, which does not affect the primary token's refresh cycle. + +### `GACAppCheckSettings` +Provides configurable settings for the `AppCheckCore` library. + +* **Purpose:** Allows customization of various behaviors, such as token refresh intervals or logging levels. + +### `GACAppCheckToken` +Represents an App Check token received from the App Check backend. + +* **Properties:** + * `token` (`NSString *`): The actual App Check token string. + * `expirationDate` (`NSDate *`): The date and time when the token expires. + +### `GACAppCheckTokenResult` +A wrapper object containing either an `GACAppCheckToken` upon success or an `NSError` upon failure. + +* **Properties:** + * `token` (`GACAppCheckToken * _Nullable`): The App Check token if the request was successful. + * `error` (`NSError * _Nullable`): An error object if the token request failed. + +## Protocols + +### `GACAppCheckProvider` +A protocol that defines the interface for App Check providers. Custom providers must conform to this protocol. + +* **Purpose:** Abstracts the specifics of how App Check tokens are obtained. Implementations interact with platform-specific attestation services or provide mock tokens. +* **Key Methods:** + * `getTokenWithCompletion:`: Asynchronously fetches a new App Check token. + * `getLimitedUseTokenWithCompletion:`: Asynchronously fetches a new limited-use App Check token. + +### `GACAppCheckTokenDelegate` +A protocol for delegates that wish to receive notifications about App Check token updates. + +* **Purpose:** Allows your application to react to changes in the App Check token, such as when a new token is fetched or an existing one is refreshed. +* **Key Methods:** + * `appCheck:didChangeToken:`: Notifies the delegate when the App Check token changes. diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 00000000..88458a96 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,97 @@ +# Architecture & Design + +This document details the internal architecture of `AppCheckCore`, +focusing on token storage, lifecycle management, and security +mechanisms. + +> [!WARNING] +> This document describes internal implementation details that are subject +> to change. Rely only on the public API surface for integration +> (see [App Check Core Documentation](index.md#important-disclaimer)). + +## Token Storage +App Check tokens are sensitive credentials that grant access to your +backend resources. `AppCheckCore` treats them with high security. + +### Keychain Storage +The `GACAppCheckStorage` class is responsible for persisting App Check +tokens. +* **Mechanism:** It uses the iOS Keychain via `GULKeychainStorage`. +* **Service Name:** `com.google.app_check_core.token_storage`. +* **Data Protection:** Tokens are stored as `GACAppCheckStoredToken` + objects (conforming to `NSSecureCoding`). +* **Access Groups:** Supports sharing tokens across apps/extensions + via Keychain Access Groups (configurable during initialization). + +### Artifact Storage (App Attest) +For the App Attest provider, intermediate artifacts are also stored to +maintain a stable device identity. +* **Class:** `GACAppAttestArtifactStorage` +* **Storage:** Keychain. +* **Key Suffix:** Keys are namespaced by the service name and resource + name (e.g., `my-sdk.projects/123/apps/abc`) to prevent collisions. + +## Token Lifecycle Management +The `GACAppCheck` class acts as the central coordinator. + +1. **Request:** The app requests a token via + `token(forcingRefresh:completion:)`. +2. **Cache Check:** + * If `forcingRefresh` is `NO`: Checks `GACAppCheckStorage` for a + valid, non-expired token. + * **Buffer Time:** Tokens are considered "expired" slightly before + their actual expiration time to account for clock skew and + network latency. +3. **Fetch (if needed):** + * If the cache is empty or expired, or `forcingRefresh` is `YES`, + a request is made to the configured `GACAppCheckProvider`. +4. **Storage:** + * Upon successful retrieval, the new token is written to + `GACAppCheckStorage`. + * Any old token is overwritten. +5. **Completion:** The token (cached or new) is returned to the caller. + +## Exponential Backoff Strategy +To prevent overwhelming the backend or Apple's servers during failures, +`AppCheckCore` implements a robust exponential backoff strategy via +`GACAppCheckBackoffWrapper`. + +### Algorithm +The backoff interval is calculated as follows: +```math +Interval = \min(Base \times Jitter, MaxInterval) +``` +* **Base:** $`2^{retryCount}`$ seconds. +* **Jitter:** A random multiplier between $1.0$ and $1.5$ (to prevent + thundering herd problems). +* **MaxInterval:** 4 hours. + +### Error Policies +The backoff behavior depends on the error type, specifically HTTP status +codes returned by the backend: + +| HTTP Status Code | Backoff Type | Reason | +| :--- | :--- | :--- | +| **< 400** | **None** | Network errors or successful requests do not trigger backoff. | +| **400 (Bad Request)**
**404 (Not Found)** | **1 Day** | Indicates a project misconfiguration or outdated app version. Unlikely to resolve quickly. | +| **403 (Forbidden)**
**429 (Too Many Requests)**
**503 (Service Unavailable)** | **Exponential** | Indicates soft deletion, rate limiting, or server overload. Retrying later is appropriate. | +| **Other 5xx** | **Exponential** | Standard server errors. | + +### Implementation +* **Class:** `GACAppCheckBackoffWrapper` +* **Usage:** Providers (`GACAppAttestProvider`, `GACDeviceCheckProvider`) + wrap their network and attestation calls in this backoff mechanism. +* **State:** The wrapper tracks the failure count and the last failure + time. It resets to 0 upon a successful token fetch. + +## Threading Model +* **Concurrency:** `AppCheckCore` is designed to be thread-safe. +* **Queues:** + * **Main Queue:** Completion handlers are typically dispatched to + the main queue. + * **Internal Queues:** Providers use private serial queues (e.g., + `com.google.GACAppAttestProvider`) to manage state and + sequentialize complex attestation flows (like generating a key, + then attesting, then exchanging). + * **Background:** Network requests are performed on background + queues (`QOS_CLASS_DEFAULT` or `QOS_CLASS_UTILITY`). \ No newline at end of file diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..74710e96 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,85 @@ +# App Check Core - Documentation + +## Introduction +`AppCheckCore` is the underlying engine for app attestation and token +management, primarily used by the Firebase iOS SDK but designed for +broader internal Google use. It provides a robust and secure way to +verify the authenticity of app instances accessing your backend +resources. This library supports applications running on iOS, macOS, +tvOS, and watchOS. + +## Key Features +* **Token Management:** Handles the lifecycle of App Check tokens, + including caching and automatic refreshing to ensure continuous + protection. +* **Provider Abstraction:** Abstracts different attestation providers, + allowing for flexible integration with various platform-specific + integrity mechanisms. +* **Limited-Use Tokens:** Supports the generation and management of + limited-use tokens for scenarios requiring single-use or short-lived + authentication. + +> [!IMPORTANT] +> **The detailed architectural and implementation documentation within this +> `docs/` directory, especially the deep dives into provider internals and +> decision logic, are provided for comprehensive understanding only.** +> +> **Consumers of the App Check Core library should exclusively rely on the +> public API surface (as defined by the public header files, e.g., in +> `AppCheckCore/Sources/Public`) for integration. Internal implementation +> details, including specific error handling flows, storage mechanisms, and +> concurrency management described herein, are subject to change without +> notice across library versions.** + +## Documentation Sections + +* [Usage Guide](usage.md): How to initialize and fetch tokens. +* [Providers Deep Dive](providers.md): Detailed architectural + breakdown of App Attest, DeviceCheck, and Debug providers, + including sequence diagrams. +* [Architecture](architecture.md): Internal design details regarding + token storage, caching strategies, and threading. +* [API Reference](api-reference.md): High-level class overview. + +## High-Level Architecture +This diagram illustrates the core relationships. For detailed sequence +flows, see [Providers Deep Dive](providers.md). + +```mermaid +classDiagram + class GACAppCheck { + +tokenForcingRefresh:completion: + +limitedUseTokenWithCompletion: + } + + class GACAppCheckProvider { + <> + +getTokenWithCompletion: + +getLimitedUseTokenWithCompletion: + } + + class GACAppAttestProvider { + -AppAttestService + -KeyStorage + -ArtifactStorage + } + + class GACDeviceCheckProvider { + -DCDevice + } + + class GACAppCheckDebugProvider { + -LocalDebugToken + } + + class GACAppCheckToken { + +token: String + +expirationDate: Date + } + + GACAppCheck "1" *-- "1" GACAppCheckProvider : uses + GACAppCheckProvider <|-- GACAppAttestProvider : implements + GACAppCheckProvider <|-- GACDeviceCheckProvider : implements + GACAppCheckProvider <|-- GACAppCheckDebugProvider : implements + GACAppCheck --> GACAppCheckToken : returns +``` \ No newline at end of file diff --git a/docs/providers.md b/docs/providers.md new file mode 100644 index 00000000..c5b38d5a --- /dev/null +++ b/docs/providers.md @@ -0,0 +1,15 @@ +# App Check Providers: Deep Dive + +This section details the internal design and detailed flows of each +App Check provider, including error handling, retries, and state +resets. + +Select a provider below for detailed documentation: + +* [AppAttest Provider](providers/app-attest.md) + * The primary provider for modern iOS devices, wrapping `DCAppAttestService`. + * Features complex state management, automatic retries, and request coalescing. +* [DeviceCheck Provider](providers/device-check.md) + * A fallback provider for older devices using `DCDevice`. +* [Debug Provider](providers/debug.md) + * For local development and CI/CD environments. diff --git a/docs/providers/app-attest.md b/docs/providers/app-attest.md new file mode 100644 index 00000000..be9f5095 --- /dev/null +++ b/docs/providers/app-attest.md @@ -0,0 +1,312 @@ +# AppAttest Provider (`GACAppAttestProvider`) + +The most complex provider, interacting with `DCAppAttestService`. It +maintains a stable key pair on the device to sign assertions. + +## Overview +The App Attest provider uses a two-phase process: + +1. **Initial Handshake (Attestation):** The SDK generates a cryptographic key + pair containing a random challenge and a Key ID. The random challenge is + provided by a Firebase backend. The Key ID is provided by an Apple backend, + via Apple's `DCAppAttestService` (from the DeviceCheck framework). The + key pair is then attested by an Apple backend, via `DCAppAttestService`, + which provides an attestation result. This attestation, Key ID, and + challenge are sent to a Firebase backend for verification and exchange + for an **App Check Token** and an **Attestation Artifact**, which is + used to validate token refreshes (assertions). +2. **Token Refresh (Assertion):** For subsequent requests, the app + uses the stored Key ID to sign a challenge (Assertion) from Apple. + The Firebase backend verifies this signature against the stored + Artifact to issue a new token. + +**Stored Artifacts:** +* **Key ID:** Identifies the key pair on the device (persisted in + UserDefaults). +* **Artifact:** An opaque object from the Firebase backend linking + the device's key to the user's session (persisted in Keychain). + +## Components +* **Service:** `DCAppAttestService` (Apple's API). +* **Storage:** + * `GACAppAttestKeyIDStorage`: Stores the generated App Attest Key + ID. + * **Location:** `UserDefaults` (Suite: `com.firebase.GACAppAttestKeyIDStorage`). + * `GACAppAttestArtifactStorage`: Stores the "artifact" returned by + the Firebase backend after a successful initial handshake. This + artifact effectively links the on-device key to the backend + session. + * **Location:** Keychain (Service: `com.firebase.app_check.app_attest_artifact_storage`). +* **Resiliency:** + * **Automatic Retry (Internal):** The provider includes an internal + retry loop (max 1 attempt) with a 0-second delay. This loop is + specifically triggered if an error wrapped as + `GACAppAttestRejectionError` occurs. + * **Triggers for Reset & Internal Retry:** + * `DCErrorInvalidKey` / `DCErrorInvalidInput` (Apple DeviceCheck error). + * HTTP 403 (Attestation Rejected) from the backend during handshake. + * **Backoff Strategy (External):** An outer `GACAppCheckBackoffWrapper` + protects the backend from traffic spikes by enforcing delays on + subsequent attempts based on the error type. + * **No Backoff (Immediately Permitted):** For non-HTTP errors (e.g., + Apple's `DCError` like `serverUnavailable`), network connectivity + issues, storage failures, or parsing errors, the backoff wrapper + **does not** enforce a delay. Subsequent `getToken` calls by the + app are immediately permitted. + * **Exponential Backoff:** Applied to retryable server errors. + * HTTP 403 (Project/App Deleted) *if internal retry fails*. + * HTTP 429 (Too Many Requests). + * HTTP 503 (Server Overloaded). + * Other HTTP 5xx (Server Errors) or 4xx not listed above or + handled by 1 day backoff. + * **1 Day Backoff:** Applied to configuration errors unlikely to + resolve quickly. + * HTTP 400 (Bad Request). + * HTTP 404 (Not Found). + * **Other Non-Resetting Errors (State Preserved):** For errors that + do not trigger the internal retry loop or an explicit external + backoff (e.g., `DCErrorServerUnavailable`, generic network issues, + storage errors), the request fails, but the App Attest key and + artifact are **preserved**. This allows the app to retry the + request later using the same key, aligning with Apple's + recommendation to preserve the device's risk metric. + +## Attestation State Calculation +The provider determines its current state by checking for the presence of +a supported environment, a stored key ID, and a stored artifact. This state +dictates whether to perform an initial handshake or a token refresh. + +```mermaid +flowchart LR + Start([Calculate State]) --> CheckSupport{Is App Attest
Supported?} + + CheckSupport -- Yes --> CheckKey{Stored Key ID?} + + CheckSupport -- No --> Unsupported["State: Unsupported"] + + CheckKey -- Yes --> CheckArtifact{Stored Artifact
for Key ID?} + + CheckKey -- No --> SupportedInitial["State: SupportedInitial
(Ready for Handshake)"] + + CheckArtifact -- Yes --> KeyRegistered["State: KeyRegistered
(Key & Artifact exist)"] + + CheckArtifact -- No --> KeyGenerated["State: KeyGenerated
(Key exists, no Artifact)"] +``` + +## Decision Logic & State Machine +Before executing a handshake, the provider determines the correct flow +based on the internal state and manages concurrent requests. + +> [!IMPORTANT] +> **Note on Limited Use:** Limited-use tokens are never reused/coalesced. +> If a limited-use token is requested (or if one is currently being +> fetched), the new request will "chain" (wait for the ongoing one to +> finish) and then start a fresh handshake to ensure a unique token is +> generated. + +```mermaid +flowchart LR + Start[getToken] --> Ongoing{Ongoing Op?} + + Ongoing -- No --> StartNew[Start New Request] + Ongoing -- Yes --> Conflict{Limited Request
or Mismatch?} + + Conflict -- Yes --> Queue[Queue New Request] + Conflict -- No --> Reuse[Reuse Existing Request] + + Queue --> Backoff + StartNew --> Backoff + + subgraph Execution ["Backoff Wrapped Execution"] + direction LR + Backoff[Check Backoff] + StateCheck["Attestation State?
(See 'Attestation State Calculation' above)"] + + Backoff --> StateCheck + + StateCheck -->|Yes| KeyCheck{Key ID?} + + KeyCheck -- No --> Flow1[Flow 1: Initial] + KeyCheck -- Yes --> ArtifactCheck{Artifact?} + + ArtifactCheck -- No --> Flow1 + ArtifactCheck -- Yes --> Flow2[Flow 2: Refresh] + + StateCheck -->|No| Error[Error] + end + + Reuse -.- Footnote["Note: The 'ongoingGetTokenOperation' tracks the active fetch.
Standard requests reuse it (unless the active fetch is Limited-use).
Limited-use requests always queue a new, sequential fetch."] + Queue -.- Footnote + StartNew -.- Footnote +``` + +## Concurrent Request Handling +The `GACAppAttestProvider` carefully manages concurrent calls to +`getToken(limitedUse:)` to ensure correctness and efficiency: + +* **No Ongoing Operation:** If no token fetching operation is in + progress, a new one is started, and its promise is stored as the + `ongoingGetTokenOperation`. +* **Reuse (Standard Tokens Only):** If a standard (non-limited use) + token is requested, and there's an `ongoingGetTokenOperation` that + is also for a standard token, the existing promise is reused. This + ensures only one actual token fetch occurs for multiple concurrent + standard requests. +* **Chaining (Limited-Use or Mismatched Requests):** + * If a limited-use token is requested, *or* + * If a standard token is requested but the `ongoingGetTokenOperation` + is for a limited-use token (or vice versa), + the new request will **chain**. This means it waits for the currently + `ongoingGetTokenOperation` to complete, and then initiates a *new*, separate + token fetching sequence. This prevents limited-use tokens from being + accidentally reused and ensures distinct token types are handled + independently. + +```mermaid +sequenceDiagram + participant AppA as App (Standard) + participant AppB as App (Limited) + participant AppC as App (Standard) + participant Provider as GACAppAttestProvider + + AppA->>Provider: getToken(false) + activate Provider + Note right of Provider: No ongoing op.
Start new op (standard).
Set ongoingGetTokenOperation. + Provider-->>Provider: Start Flow 1/2 sequence + + AppB->>Provider: getToken(true) + activate Provider + Note right of Provider: Ongoing op (standard) exists.
New request is limited-use.
Chain: Wait for ongoing, then start new op. + Provider->>Provider: Await ongoing op completion + deactivate Provider + + AppC->>Provider: getToken(false) + activate Provider + Note right of Provider: Ongoing op (standard) exists.
New request is standard.
Reuse ongoing op's promise. + Provider-->>AppC: App Check Token (from ongoing op) + deactivate Provider + + Provider-->>AppA: App Check Token (from completed op) + deactivate Provider + + Provider-->>Provider: Start new Flow 1/2 for App (Limited) + activate Provider + Provider-->>AppB: App Check Token (from new op) + deactivate Provider +``` + +## Flow 1: Initial Handshake (Attestation) +Occurs when the app runs for the first time, or if the stored artifact + +| Component | Details | +| :--- | :--- | +| **Inputs** | `limitedUse` (Bool)
Optional existing `Key ID` | +| **Outputs** | `App Check Token` (Returned)
`Key ID` (Persisted)
`Artifact` (Persisted) | + +> [!NOTE] +> **Note on Error Handling:** Errors not explicitly handled in this flow +> (e.g., network issues, storage failures) will result in the promise being +> rejected. Such errors may be subject to external backoff if applicable, +> and all errors eventually bubble up to the caller (unless successfully +> resolved by an internal retry). + +```mermaid +sequenceDiagram + participant App + participant Provider as GACAppAttestProvider + participant Apple as DCAppAttestService

(Apple's DeviceCheck Framework) + participant AppleServer as Apple Server + participant API as GACAppAttestAPIService + participant Backend as Firebase Backend + + App->>Provider: getToken(limitedUse) + + loop Retry Loop (Max 1 Retry for GACAppAttestRejectionError) + par Parallel Execution + Provider->>API: getRandomChallenge() + API->>Backend: POST /generateAppAttestChallenge + Backend-->>API: { "challenge": "..." } + API-->>Provider: Challenge + and + Provider->>Apple: generateKey() (If needed) + Apple-->>Provider: Key ID + end + + Provider->>Apple: attestKey(keyId, clientDataHash=SHA256(challenge)) + Apple->>AppleServer: Contact App Attest Service + AppleServer-->>Apple: Attestation Result + + alt Attestation Failed (Invalid Key/Input) + Apple-->>Provider: DCErrorInvalidKey / Input + Provider->>Provider: RESET: Delete KeyID & Artifact + Note right of Provider: Throws GACAppAttestRejectionError,
Triggering Loop Retry + else Attestation Success + Apple-->>Provider: Attestation Object + Provider->>API: attestKeyWithAttestation(attestation, keyID, challenge, limitedUse) + API->>Backend: POST /exchangeAppAttestAttestation
{ limited_use: true/false } + + alt Backend Rejection (403) + Backend-->>API: 403 Forbidden + API-->>Provider: Error (403) + Provider->>Provider: RESET: Delete KeyID & Artifact + Note right of Provider: Throws GACAppAttestRejectionError,
Triggering Loop Retry + else Success + Backend-->>API: { "token": "...", "artifact": "..." } + API-->>Provider: { "token": "...", "artifact": "..." } + Provider->>Provider: Store Artifact & Key ID + Provider-->>App: App Check Token + end + end + end +``` + +## Flow 2: Token Refresh (Assertion) +Occurs for subsequent requests using the established key pair. + +| Component | Details | +| :--- | :--- | +| **Inputs** | `limitedUse` (Bool)
`Key ID` (From Storage)
`Artifact` (From Storage) | +| **Outputs** | `App Check Token` (Returned) | + +> [!NOTE] +> **Note on Error Handling:** Errors not explicitly handled in this flow +> (e.g., network issues, storage failures) will result in the promise being +> rejected. Such errors may be subject to external backoff if applicable, +> and all errors eventually bubble up to the caller (unless successfully +> resolved by an internal retry). + +```mermaid +sequenceDiagram + participant App + participant Provider as GACAppAttestProvider + participant Apple as DCAppAttestService

(Apple's DeviceCheck Framework) + participant API as GACAppAttestAPIService + participant Backend as Firebase Backend + + App->>Provider: getToken(limitedUse) + + loop Retry Loop (Max 1 Retry for GACAppAttestRejectionError) + Provider->>Provider: Retrieve Key ID & Artifact + Provider->>API: getRandomChallenge() + API->>Backend: POST /generateAppAttestChallenge + Backend-->>API: { "challenge": "..." } + API-->>Provider: Challenge + + Provider->>Provider: ClientData = Artifact + Challenge + Provider->>Apple: generateAssertion(keyId, clientDataHash=SHA256(ClientData)) + + alt Assertion Failed (Invalid Key/Input) + Apple-->>Provider: DCErrorInvalidKey / Input + Provider->>Provider: RESET: Delete KeyID & Artifact + Note right of Provider: Throws GACAppAttestRejectionError,
Triggering Loop Retry
(Will fall back to Initial Handshake) + else Assertion Success + Apple-->>Provider: Assertion Object + + Provider->>API: getAppCheckTokenWithArtifact(..., limitedUse) + API->>Backend: POST /exchangeAppAttestAssertion
{ limited_use: true/false } + Backend-->>API: { "token": "..." } + + Provider-->>App: App Check Token + end + end +``` \ No newline at end of file diff --git a/docs/providers/debug.md b/docs/providers/debug.md new file mode 100644 index 00000000..82c71747 --- /dev/null +++ b/docs/providers/debug.md @@ -0,0 +1,30 @@ +# Debug Provider (`GACAppCheckDebugProvider`) + +Used for local development and CI. + +## Configuration +The provider looks for a debug secret in the following order: +1. **Environment Variable:** `AppCheckDebugToken` (or legacy + `FIRAAppCheckDebugToken`). +2. **Local Storage:** `NSUserDefaults` key `GACAppCheckDebugToken`. +3. **Generation:** If neither exists, it generates a new UUID, stores it + in `NSUserDefaults`, and logs it to the console (warning level). + +## Flow +```mermaid +sequenceDiagram + participant App + participant Provider as GACAppCheckDebugProvider + participant API as GACAppCheckDebugProviderAPIService + participant Backend as Firebase Backend + + App->>Provider: getToken(limitedUse) + Provider->>Provider: Determine Debug Secret (Env Var or UUID) + + Provider->>API: appCheckTokenWithDebugToken(debugToken, limitedUse) + API->>Backend: POST /exchangeDebugToken
{ limited_use: true/false } + Note right of Backend: Checks if debug token is
registered in Console. + Backend-->>API: { "token": "..." } + + Provider-->>App: App Check Token +``` diff --git a/docs/providers/device-check.md b/docs/providers/device-check.md new file mode 100644 index 00000000..d43ae48e --- /dev/null +++ b/docs/providers/device-check.md @@ -0,0 +1,36 @@ +# DeviceCheck Provider (`GACDeviceCheckProvider`) + +A simpler provider for older devices. + +## Components +* **Service:** `DCDevice` (Apple's API). +* **Generator:** `DCDevice.currentDevice`. + +## Flow +```mermaid +sequenceDiagram + participant App + participant Provider as GACDeviceCheckProvider + participant Apple as DCDevice

(Apple's DeviceCheck Framework) + participant API as GACDeviceCheckAPIService + participant Backend as Firebase Backend + + App->>Provider: getToken(limitedUse) + + Note right of Provider: Wrapped in Backoff Wrapper + Provider->>Apple: generateToken() + Apple-->>Provider: Device Token (Ephemeral) + + Provider->>API: appCheckTokenWithDeviceToken(deviceToken, limitedUse) + API->>Backend: POST /exchangeDeviceCheckToken
{ limited_use: true/false } + Note right of Backend: Verifies device token with Apple. + + alt Error (e.g., 503) + Backend-->>API: 503 Service Unavailable + Provider->>Provider: Record Failure (Backoff) + Provider-->>App: Error + else Success + Backend-->>API: { "token": "..." } + Provider-->>App: App Check Token + end +``` diff --git a/docs/usage.md b/docs/usage.md new file mode 100644 index 00000000..c7a79176 --- /dev/null +++ b/docs/usage.md @@ -0,0 +1,144 @@ +# Core Integration and Usage + +## Initialization + +To use `AppCheckCore`, you first need to initialize an `AppCheckCoreProvider` implementation (e.g., `AppCheckCoreAppAttestProvider`) and then use it to initialize `AppCheckCore`. + +### Swift +```swift +import AppCheckCore +import Foundation + +// 1. Initialize an App Check Provider +// Replace "my-sdk" with your SDK's unique identifier. +// Replace "projects/123/apps/abc" with your actual resource name. +// Set baseURL and APIKey if needed, otherwise nil. +// Provide a keychainAccessGroup if you use one. +let provider = AppCheckCoreAppAttestProvider(serviceName: "my-sdk", + resourceName: "projects/123/apps/abc", + baseURL: nil, + APIKey: nil, + keychainAccessGroup: nil, + requestHooks: nil) + +// 2. Initialize AppCheckCore +// You can optionally provide settings and a token delegate. +let appCheck = AppCheckCore(serviceName: "my-sdk", + resourceName: "projects/123/apps/abc", + appCheckProvider: provider, + settings: AppCheckCoreSettings(), // Use default settings or provide your own + tokenDelegate: nil, // Provide a delegate to observe token changes + keychainAccessGroup: nil) +``` + +### Objective-C +```objectivec +#import +#import + +// 1. Initialize an App Check Provider +// Replace "my-sdk" with your SDK's unique identifier. +// Replace "projects/123/apps/abc" with your actual resource name. +// Set baseURL and APIKey if needed, otherwise nil. +// Provide a keychainAccessGroup if you use one. +GACAppAttestProvider *provider = + [[GACAppAttestProvider alloc] initWithServiceName:@"my-sdk" + resourceName:@"projects/123/apps/abc" + baseURL:nil + APIKey:nil + keychainAccessGroup:nil + requestHooks:nil]; + +// 2. Initialize AppCheckCore +// You can optionally provide settings and a token delegate. +GACAppCheck *appCheck = + [[GACAppCheck alloc] initWithServiceName:@"my-sdk" + resourceName:@"projects/123/apps/abc" + appCheckProvider:provider + settings:[[GACAppCheckSettings alloc] init] // Use default settings or provide your own + tokenDelegate:nil // Provide a delegate to observe token changes + keychainAccessGroup:nil]; +``` + +## Fetching Tokens + +`AppCheckCore` provides methods to fetch App Check tokens, both for general use and for limited-use scenarios. + +### Fetching a Standard App Check Token +Use `token(forcingRefresh:completion:)` to retrieve an App Check token. The `forcingRefresh` parameter determines whether to use a cached token or request a new one. In most cases, `NO` (or `false` in Swift) should be used. + +### Swift +```swift +appCheck.token(forcingRefresh: false) { result in + if let token = result.token { + print("App Check Token: \(token.token)") + print("Token Expiration: \(token.expirationDate)") + } else if let error = result.error { + print("Error fetching App Check token: \(error.localizedDescription)") + } +} +``` + +### Objective-C +```objectivec +[appCheck tokenForcingRefresh:NO + completion:^(GACAppCheckTokenResult *result) { + if (result.token) { + NSLog(@"App Check Token: %@", result.token.token); + NSLog(@"Token Expiration: %@", result.token.expirationDate); + } else if (result.error) { + NSLog(@"Error fetching App Check token: %@", result.error.localizedDescription); + } +}]; +``` + +### Fetching a Limited-Use App Check Token +For scenarios where you need a token for a single, immediate request without affecting the primary token's refresh cycle, use `limitedUseToken(completion:)`. + +### Swift +```swift +appCheck.limitedUseTokenWithCompletion { result in + if let token = result.token { + print("Limited-Use App Check Token: \(token.token)") + } else if let error = result.error { + print("Error fetching limited-use App Check token: \(error.localizedDescription)") + } +} +``` + +### Objective-C +```objectivec +[appCheck limitedUseTokenWithCompletion:^(GACAppCheckTokenResult *result) { + if (result.token) { + NSLog(@"Limited-Use App Check Token: %@", result.token.token); + } else if (result.error) { + NSLog(@"Error fetching limited-use App Check token: %@", result.error.localizedDescription); + } +}]; +``` + +## Token Fetching Sequence Diagram +This diagram illustrates the typical flow when an application requests an App Check token. + +```mermaid +sequenceDiagram + participant App + participant GACAppCheck + participant Cache + participant GACAppCheckProvider + participant AppleBackend + + App->>GACAppCheck: Request Token (forcingRefresh: false) + GACAppCheck->>Cache: Check for valid token + alt Token in cache and valid + Cache-->>GACAppCheck: Cached Token + GACAppCheck-->>App: App Check Token Result + else Token missing or expired + GACAppCheck->>GACAppCheckProvider: Get Token + GACAppCheckProvider->>AppleBackend: Perform Attestation/Verification + AppleBackend-->>GACAppCheckProvider: Attestation/Verification Result + GACAppCheckProvider-->>GACAppCheck: New App Check Token + GACAppCheck->>Cache: Store New Token + GACAppCheck-->>App: App Check Token Result + end +``` \ No newline at end of file