device:<hash>) and mints a short-lived client token for it. The only credential your app ships is a publishable key, which is mint-only.
How the flow works
Device auth is a three-step handshake against publishable-key-only routes:POST /v1/device/challenge— the gateway issues a single-use challenge (valid ~5 minutes).POST /v1/device/attest— on first launch, the app returns its App Attest attestation over the challenge. The gateway verifies it against Apple’s pinned App Attest root, stores the device’s public key and sign counter, and mints a token for the per-device end user.POST /v1/device/assert— a returning device proves liveness with an assertion (a signature over a fresh challenge plus a strictly increasing counter that defeats replay), and mints again.
authentication_error, so a probe learns nothing about which check failed. The abuse bound lives on challenge issuance, which is covered by the same per-key and per-IP mint limiter as the backend mint route.
1. Enable device auth for the project
Device auth is off by default and enabled per project. Turn it on and supply your app’s public identity:- Apple Team ID — the 10-character team identifier.
- App bundle id — your app’s reverse-DNS bundle identifier.
- App Attest environment —
productionordevelopment(which AAGUID a genuine attestation must carry).
rd_pk_…) for the environment; it is the only Roadie credential the app embeds.
2. Wire up RoadieKit on iOS
RoadieKit ships anAppAttestTokenProvider that performs the whole handshake for you — generate/attest the key on first run, assert on later runs, cache the token in memory, and re-mint before expiry or after a 401. Only the App Attest key id is persisted (Keychain); the token never is.
App Attest requires a real device — it does not run in the iOS Simulator or on CI. RoadieKit puts
the
DCAppAttestService behind a protocol so tests can inject a fake.3. Or drive the REST flow directly
If you are not using RoadieKit, call the three routes yourself with the publishable key as the bearer. Thekey_id is your App Attest key id; challenge is the value from step 1; attestation / assertion are base64-encoded. The mint response is { token, expires_at, end_user_id }.
Anti-farming: the DeviceCheck free-tier guard
An optional Apple DeviceCheck token can travel alongside the attestation (device_check_token). Apple persists two bits per device that survive reinstalls and device erase, so Roadie can stop a device from farming a fresh free tier by reinstalling. It is:
- Optional and fail-open — an older SDK, an unsupported device, or a token error simply grants the normal free tier and never blocks the mint.
- Off by default and configured per project (it needs your DeviceCheck signing key).
free_exhausted tier, which paywalls it until the UTC month rolls over — or until you grant it pro. That is the bridge to Free → Pro with entitlements.
What you get
- Short-lived, per-device client tokens with no secret shipped in the app.
- Each device is a distinct end user, so per-user limits and blocking apply per device.
- A clean upgrade path from an anonymous free device to a paying customer via entitlements.
Related
Client tokens
How device attestation mints a token.
App Attest tutorial
The full device sign-in walkthrough.
Free → Pro entitlements
Turn a free device into a paying user.
iOS SDK — RoadieKit
The Swift SDK reference.