Skip to content

Commit abfa6d3

Browse files
committed
review
1 parent f1c4123 commit abfa6d3

2 files changed

Lines changed: 100 additions & 46 deletions

File tree

docs/architecture.md

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,36 @@ The `GACAppCheck` class acts as the central coordinator.
3232
* Any old token is overwritten.
3333
5. **Completion:** The token (cached or new) is returned to the caller.
3434

35-
## Backoff Strategy
36-
To prevent overwhelming the backend or Apple's servers during failures, `AppCheckCore` implements an exponential backoff strategy.
35+
## Exponential Backoff Strategy
36+
To prevent overwhelming the backend or Apple's servers during failures, `AppCheckCore` implements a robust exponential backoff strategy via `GACAppCheckBackoffWrapper`.
37+
38+
### Algorithm
39+
The backoff interval is calculated as follows:
40+
$$
41+
\text{Interval} = \min(\text{Base} \times \text{Jitter}, \text{MaxInterval})
42+
$$
43+
* **Base:** $2^{\text{retry\_count}}$ seconds.
44+
* **Jitter:** A random multiplier between $1.0$ and $1.5$ (to prevent thundering herd problems).
45+
* **MaxInterval:** 4 hours.
46+
47+
### Error Policies
48+
The backoff behavior depends on the error type, specifically HTTP status codes returned by the backend:
49+
50+
| HTTP Status Code | Backoff Type | Reason |
51+
| :--- | :--- | :--- |
52+
| **< 400** | **None** | Network errors or successful requests do not trigger backoff. |
53+
| **400 (Bad Request)**<br>**404 (Not Found)** | **1 Day** | Indicates a project misconfiguration or outdated app version. Unlikely to resolve quickly. |
54+
| **403 (Forbidden)**<br>**429 (Too Many Requests)**<br>**503 (Service Unavailable)** | **Exponential** | Indicates soft deletion, rate limiting, or server overload. Retrying later is appropriate. |
55+
| **Other 5xx** | **Exponential** | Standard server errors. |
56+
57+
### Implementation
3758
* **Class:** `GACAppCheckBackoffWrapper`
3859
* **Usage:** Providers (`GACAppAttestProvider`, `GACDeviceCheckProvider`) wrap their network and attestation calls in this backoff mechanism.
39-
* **Behavior:** Retries with increasing delays on retryable errors (e.g., network timeouts, temporary server errors 503). Non-retryable errors (e.g., 403 Forbidden, 400 Bad Request) fail immediately.
60+
* **State:** The wrapper tracks the failure count and the last failure time. It resets to 0 upon a successful token fetch.
4061

4162
## Threading Model
4263
* **Concurrency:** `AppCheckCore` is designed to be thread-safe.
4364
* **Queues:**
44-
* **Main Queue:** Completion handlers are typically dispatched to the main queue (or a user-specified queue if the API supported it, but currently defaults to main for top-level APIs).
65+
* **Main Queue:** Completion handlers are typically dispatched to the main queue.
4566
* **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).
46-
* **Background:** Network requests are performed on background queues (`QOS_CLASS_DEFAULT` or `QOS_CLASS_UTILITY`).
67+
* **Background:** Network requests are performed on background queues (`QOS_CLASS_DEFAULT` or `QOS_CLASS_UTILITY`).

docs/providers.md

Lines changed: 74 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# App Check Providers: Deep Dive
22

3-
This document details the internal design and detailed flows of each App Check provider.
3+
This document details the internal design and detailed flows of each App Check provider, including error handling, retries, and state resets.
44

55
## AppAttest Provider (`GACAppAttestProvider`)
66
The most complex provider, interacting with `DCAppAttestService`. It maintains a stable key pair on the device to sign assertions.
@@ -9,10 +9,12 @@ The most complex provider, interacting with `DCAppAttestService`. It maintains a
99
* **Service:** `DCAppAttestService` (Apple's API).
1010
* **Storage:**
1111
* `GACAppAttestKeyIDStorage`: Stores the generated App Attest Key ID.
12-
* `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.
12+
* `GACAppAttestArtifactStorage`: Stores the "artifact" returned by the Firebase backend after a successful initial handshake.
13+
* **Resiliency:**
14+
* **Automatic Retry:** The provider wraps the entire flow in a retry loop. If a specific "Rejection Error" occurs (e.g., invalid key), it resets its internal state and retries the flow from scratch.
1315

1416
### Flow 1: Initial Handshake (Attestation)
15-
Occurs when the app runs for the first time or if the stored artifact is missing/corrupted.
17+
Occurs when the app runs for the first time, or if the stored artifact is missing, or **after a reset**.
1618

1719
```mermaid
1820
sequenceDiagram
@@ -23,31 +25,45 @@ sequenceDiagram
2325
participant Backend as Firebase Backend
2426
2527
App->>Provider: getToken()
26-
Provider->>API: getRandomChallenge()
27-
API->>Backend: POST /generateAppAttestChallenge
28-
Backend-->>API: { "challenge": "..." }
2928
30-
par Parallel Execution
31-
Provider->>Apple: generateKey()
32-
Apple-->>Provider: Key ID
33-
and
34-
Provider->>API: (Challenge received)
29+
loop Retry Loop (Max 1 Retry)
30+
Provider->>API: getRandomChallenge()
31+
API->>Backend: POST /generateAppAttestChallenge
32+
Backend-->>API: { "challenge": "..." }
33+
34+
par Parallel Execution
35+
Provider->>Apple: generateKey() (If needed)
36+
Apple-->>Provider: Key ID
37+
and
38+
Provider->>API: (Challenge received)
39+
end
40+
41+
Provider->>Apple: attestKey(keyId, clientDataHash=SHA256(challenge))
42+
43+
alt Attestation Failed (Invalid Key/Input)
44+
Apple-->>Provider: DCErrorInvalidKey / Input
45+
Provider->>Provider: RESET: Delete KeyID & Artifact
46+
Note right of Provider: Throws RejectionError,<br/>Triggering Loop Retry
47+
else Attestation Success
48+
Apple-->>Provider: Attestation Object
49+
Provider->>API: attestKeyWithAttestation(...)
50+
API->>Backend: POST /exchangeAppAttestAttestation
51+
52+
alt Backend Rejection (403)
53+
Backend-->>API: 403 Forbidden
54+
Provider->>Provider: RESET: Delete KeyID & Artifact
55+
Note right of Provider: Throws RejectionError,<br/>Triggering Loop Retry
56+
else Success
57+
Backend-->>API: { "token": "...", "artifact": "..." }
58+
Provider->>Provider: Store Artifact & Key ID
59+
Provider-->>App: App Check Token
60+
end
61+
end
3562
end
36-
37-
Provider->>Apple: attestKey(keyId, clientDataHash=SHA256(challenge))
38-
Apple-->>Provider: Attestation Object
39-
40-
Provider->>API: attestKeyWithAttestation(attestation, keyID, challenge)
41-
API->>Backend: POST /exchangeAppAttestAttestation
42-
Note right of Backend: Verifies attestation validity <br/>and app integrity.
43-
Backend-->>API: { "token": "...", "artifact": "..." }
44-
45-
Provider->>Provider: Store Artifact & Key ID
46-
Provider-->>App: App Check Token
4763
```
4864

4965
### Flow 2: Token Refresh (Assertion)
50-
Occurs for subsequent requests. It's faster and uses the established key pair.
66+
Occurs for subsequent requests using the established key pair.
5167

5268
```mermaid
5369
sequenceDiagram
@@ -58,21 +74,30 @@ sequenceDiagram
5874
participant Backend as Firebase Backend
5975
6076
App->>Provider: getToken()
61-
Provider->>API: getRandomChallenge()
62-
API->>Backend: POST /generateAppAttestChallenge
63-
Backend-->>API: { "challenge": "..." }
64-
65-
Provider->>Provider: Retrieve stored Artifact
66-
Provider->>Provider: ClientData = Artifact + Challenge
67-
Provider->>Apple: generateAssertion(keyId, clientDataHash=SHA256(ClientData))
68-
Apple-->>Provider: Assertion Object
69-
70-
Provider->>API: getAppCheckTokenWithArtifact(artifact, challenge, assertion)
71-
API->>Backend: POST /exchangeAppAttestAssertion
72-
Note right of Backend: Verifies assertion signature <br/>matches stored public key.
73-
Backend-->>API: { "token": "..." }
7477
75-
Provider-->>App: App Check Token
78+
loop Retry Loop (Max 1 Retry)
79+
Provider->>API: getRandomChallenge()
80+
API->>Backend: POST /generateAppAttestChallenge
81+
Backend-->>API: { "challenge": "..." }
82+
83+
Provider->>Provider: Retrieve stored Artifact
84+
Provider->>Provider: ClientData = Artifact + Challenge
85+
Provider->>Apple: generateAssertion(keyId, clientDataHash=SHA256(ClientData))
86+
87+
alt Assertion Failed (Invalid Key/Input)
88+
Apple-->>Provider: DCErrorInvalidKey / Input
89+
Provider->>Provider: RESET: Delete KeyID & Artifact
90+
Note right of Provider: Throws RejectionError,<br/>Triggering Loop Retry<br/>(Will fall back to Initial Handshake)
91+
else Assertion Success
92+
Apple-->>Provider: Assertion Object
93+
94+
Provider->>API: getAppCheckTokenWithArtifact(...)
95+
API->>Backend: POST /exchangeAppAttestAssertion
96+
Backend-->>API: { "token": "..." }
97+
98+
Provider-->>App: App Check Token
99+
end
100+
end
76101
```
77102

78103
---
@@ -82,7 +107,7 @@ A simpler provider for older devices.
82107

83108
### Components
84109
* **Service:** `DCDevice` (Apple's API).
85-
* **Generator:** `DCDevice.currentDevice` (can be mocked for testing).
110+
* **Generator:** `DCDevice.currentDevice`.
86111

87112
### Flow
88113
```mermaid
@@ -94,15 +119,23 @@ sequenceDiagram
94119
participant Backend as Firebase Backend
95120
96121
App->>Provider: getToken()
122+
123+
Note right of Provider: Wrapped in Backoff Wrapper
97124
Provider->>Apple: generateToken()
98125
Apple-->>Provider: Device Token (Ephemeral)
99126
100127
Provider->>API: appCheckTokenWithDeviceToken(deviceToken)
101128
API->>Backend: POST /exchangeDeviceCheckToken
102129
Note right of Backend: Verifies device token with Apple.
103-
Backend-->>API: { "token": "..." }
104130
105-
Provider-->>App: App Check Token
131+
alt Error (e.g., 503)
132+
Backend-->>API: 503 Service Unavailable
133+
Provider->>Provider: Record Failure (Backoff)
134+
Provider-->>App: Error
135+
else Success
136+
Backend-->>API: { "token": "..." }
137+
Provider-->>App: App Check Token
138+
end
106139
```
107140

108141
---
@@ -133,4 +166,4 @@ sequenceDiagram
133166
Backend-->>API: { "token": "..." }
134167
135168
Provider-->>App: App Check Token
136-
```
169+
```

0 commit comments

Comments
 (0)