iOSのリセマラを防ぐ


# 要約

DeviceCheckを使ってリセマラを防ぎます

# 概要

リセマラ（リセットマラソン）を防ぐために、DeviceCheckを使って端末チェックを行います。
ここでいうリセマラとは、アプリをアンインストール/インストールを繰り返して、初回限定の利益を何度も受ける行為を指します。

端末チェックの結果、「この端末ではまだ初回限定の利益を受けていない」と判断できた場合、初回限定の特典を与えます。
DeviceCheckを使った端末チェックはiOSアプリ単体で完結するわけではなく、さまざまな手順が必要になります。
また、仕組み上、中古端末や譲渡を受けた端末では初回限定の判断はできないかと思われます。

# アプリの流れ
1. iOSアプリがDeviceCheckを利用して、トークンを発行する
1. iOSアプリは、サーバーに対してトークンを送信する
1. サーバーは、JWTを生成して、トークンとともにApple APIへリクエストを送る
1. サーバーは、Apple APIの結果を受け取り、iOSアプリへ返す
1. サーバーのレスポンスを元に、初回限定の利益を与える/与えない

# 端末チェックの手順
## p8編
iOS Developer centerで、Keysを作成することができる権限のアカウントが必要です。
iOS Developer centerでDeviceCheck用のP8ファイルを作成する必要があります。
下記は手順の一例です。iOS Developer centerに変更が行われている場合は適宜読み替えてください。

- Certificates, Identifiers & Profiles の Keysを選択
- Create Keyを押下
- Register a New Keyで、Key Nameを任意、DeviceCheckをチェック
- Downloadで、p8ファイルをダウンロードする。これはサーバーサイドで利用します。

## サーバ編
先ほどダウンロードしたp8ファイルが必要です。

サーバーサイドでは、`p8ファイル`、`iOS Developer centerで生成したKeyのKey ID`、`iOS Developer centerのチームID`が必要になり、組み合わせてJWTを生成し、iOSアプリからデバイストークン受信時、Authorizationヘッダーに `Bearer <GeneratedJWT>` を付与して、bodyにPayloadを付与してAppleのAPIへリクエストを行います。

APIには `query_two_bits` と `update_two_bits` があり、まず `query_two_bits` を確認して、`200 Bit State Not Found`であれば初回限定とみなせます。
必要であればこの時点で`update_two_bits`で初回特典を付与済みとすればよいでしょう。

AppleのDevice check用APIについての公式はこちらです。

https://developer.apple.com/documentation/devicecheck/accessing_and_modifying_per-device_data

JWTを生成するライブラリは各種環境で選定してください。
以下はJWT生成時のHEADERとPAYLOADの例です。

HEADER例

```
{
  "alg": "ES256",
  "kid": "(iOS Developer centerで生成したKeyのKey ID)"
}
```

PAYLOAD例

```
{
  "iss": "(iOS Developer centerのチームID)",
  "iat": 1573712797,
  "exp": 1573716397
}
```

bodyの例

```
{
	"device_token": "Base 64エンコードされた、iOSから送られてきたデバイストークン",
	"transaction_id": "C94E26BD-714B-4D3F-8733-E86A790C0F8D",
	"timestamp": 1573707063000
}
```


## iOSアプリ編
DeviceCheckは実機じゃないと動作しません。プロビジョニングもDeviceCheckのp8ファイルを作ったチームIDと等しい必要があります。
iOSアプリでは `DCDevice.current.generateToken` を実行し、生成したトークンをサーバーへ送信し、その結果を受け取って初回限定の特典を与えることになります。

```swift
import DeviceCheck
        DCDevice.current.generateToken { (deviceToken, error) in
            // TODO: サーバーにdeviceTokenを送信して、レスポンスを受け取って処理をする
        }
```

### iOS単体での実機デバッグ
DeviceCheckは、サーバーと連携して使うことが前提のものですが、iOSアプリ単体でテストとして使いたい場合、[CupertinoJWT](https://github.com/ethanhuang13/CupertinoJWT) を使ってApple APIとの通信が可能です。
下記は`query_two_bits`の例です。
あくまでテストとしてお使いください。

```swift
import DeviceCheck
import CupertinoJWT

        DCDevice.current.generateToken { (deviceToken, error) in
            //p8ファイルの中身
            let p8 = """
                    -----BEGIN PRIVATE KEY-----
                    ...
                    -----END PRIVATE KEY-----
                    """
            let keyID = "iOS Developer centerで生成したKeyのKey ID"
            let teamID = "iOS Developer centerのチームID"
            let jwt = JWT(keyID: keyID, teamID: teamID, issueDate: Date(), expireDuration: 60 * 60)

            do {
                let generatedJWT = try jwt.sign(with: p8)
                let url = URL(string: "https://api.development.devicecheck.apple.com/v1/query_two_bits")!
                var request = URLRequest(url: url)
                request.httpMethod = "POST"

                let deviceTokenString = deviceToken!.base64EncodedString(options: Data.Base64EncodingOptions.init())
                let timestamp:Int = Int(Date().timeIntervalSince1970)
                let transaction = UUID().uuidString
                let requestBody = """
                {
                   "device_token" : "\(deviceTokenString)",
                   "transaction_id" : "\(transaction)",
                   "timestamp" : \(timestamp)000
                }
                """
                let data = requestBody.data(using: .utf8)
                request.addValue("Bearer \(generatedJWT)", forHTTPHeaderField: "Authorization")

                request.httpBody = data
                let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
                    // NOTE:Apple Device Check APIの結果を判断して初回限定の付与などを判定
                }
                task.resume()
            } catch {
                // Handle error
            }
        }
```