菜鸟教程小白 发表于 2022-12-11 19:02:01

ios - 如何针对 iCloud 验证 iCloud ID token ?


                                            <p><p>我是 <a href="https://medium.com/@skreutzb/ios-onboarding-without-signup-screens-cb7a76d01d6e" rel="noreferrer noopener nofollow">just reading</a>关于在移动设备上使用 iCloud IDtoken 进行应用识别。</p>

<p>如果我的服务器通过 Internet 接收到带有 iCloud IDtoken 的请求,是否有办法验证它是由 Apple 发出的,而不是由发送方编造的?</p></p>
                                    <br><hr><h1><strong>Best Answer-推荐答案</ strong></h1><br>
                                            <p><p>看看<a href="https://developer.apple.com/documentation/devicecheck" rel="noreferrer noopener nofollow">Device Check Framework.</a> “访问您的关联服务器可以在其业务逻辑中使用的每个设备、每个开发人员的数据。”在最近对 <a href="https://softwareengineering.stackexchange.com/questions/219028/how-to-safeguard-a-rest-api-for-only-trusted-mobile-applications" rel="noreferrer noopener nofollow">this SO thread</a> 中的答案的评论中提出了建议。 .</p>

<p>这是如何使用带有 iCloud 用户 ID 哈希的设备检查来确保对您的 API 的请求是合法的。以下很多代码改编自<a href="https://github.com/marinosoftware/DeviceCheckSample/blob/master/app/MarinoDeviceCheck/ViewController.swift" rel="noreferrer noopener nofollow">this</a> .</p>

<ol>
<li><p>在您的 iOS 应用中从 Apple 获取一个临时的 Device Checktoken ,然后将其与您的请求以及 iCloud 用户名哈希一起发送到您的后端。</p>

<p>在 Swift 4 中:</p>

<pre><code>import DeviceCheck

let currDevice = DCDevice.current

if ViewController.currDevice.isSupported {
    ViewController.currDevice.generateToken { (data, error) in
      if let data = data {
            let url = &#34;your-url&#34;
            let sesh = URLSession(configuration: .default)
            var req = URLRequest(url: url)
            req.addValue(&#34;application/json&#34;, forHTTPHeaderField: &#34;Content-Type&#34;)
            req.httpMethod = &#34;POST&#34;
            DispatchQueue.main.sync {
                var jsonObj = [
                  &#34;deviceCheckToken&#34; : data.base64EncodedString(),
                  &#34;iCloudUserNameHash&#34;: self.iCloudUserID,
                  &#34;moreParams&#34;: &#34;moreParamsHere&#34;
                ]
                let data = try! JSONSerialization.data(withJSONObject: jsonObj, options: [])
                req.httpBody = data
                let task = sesh.dataTask(with: req, completionHandler: { (data, response, error) in
                  if let data = data, let jsonData = try? JSONSerialization.jsonObject(with: data, options: .mutableContainers), let jsonDictionary = jsonData as? {
                        DispatchQueue.main.async {
                            // Process response here
                        }
                  }
                })
                task.resume()
            }
      } else if let error = error {
            print(&#34;Error when generating a token:&#34;, error.localizedDescription)
      }
    }
} else {
    print(&#34;Platform is not supported. Make sure you aren&#39;t running in an emulator.&#34;)
}
</code></pre> </li>
<li><p>您可以在设备检查框架中为每个设备的每个应用存储两个位。使用 bit0 记住您已经向当前设备提供了请求。首先调用 Device Check 验证端点以查看请求是否来自 iOS 应用程序——而不是例如某人的终端。接下来,使用 Device Check 查询端点获取当前设备的两个 Device Check 位。如果 bit0 为真,则假设此设备在您的请求表中已经至少有一行键入了给定的 iCloud 用户名哈希。如果有这样的一行,这可能是一个合法的请求,因为很难猜出其他键。如果没有这样的行,用户可能生成了一个虚假的 iCloud 用户哈希。但是,如果 bit0 为 false,则该设备尚未在 requests 表中放入一行。在给定的 iCloud 用户名哈希上键入一行,并使用 Device Check 更新端点将此设备的 bit0 设置为 true。这是 AWS Lambda 中节点 8.10 中的一个示例,其中 requests 表位于 DynamoDB 中。</p>

<p><strong>endpoint.js</strong></p>

<pre><code>const AWS = require(&#39;aws-sdk&#39;);
const utf8 = require(&#39;utf8&#39;);

const asyncAWS = require(&#39;./lib/awsPromiseWrappers&#39;);
const deviceCheck = require(&#39;./lib/deviceCheck&#39;);
const util = require(&#39;./lib/util&#39;);

// AWS globals
const lambda = new AWS.Lambda({
    region: process.env.AWS_REGION,
});
const dynamodb = new AWS.DynamoDB.DocumentClient();

// Apple Device Check keys
const cert = utf8.encode([
    process.env.BEGIN_PRIVATE_KEY,
    process.env.APPLE_DEVICE_CHECK_CERT,
    process.env.END_PRIVATE_KEY,
].join(&#39;\n&#39;));// utf8 encoding and newlines are necessary for jwt to do job
const keyId = process.env.APPLE_DEVICE_CHECK_KEY_ID;
const teamId = process.env.APPLE_ITUNES_CONNECT_TEAM_ID;

// Return true if device check succeeds
const isLegitDevice = async (deviceCheckToken, iCloudUserNameHash) =&gt; {

    // Pick the correct (dev or prod) Device Check API URL
    var deviceCheckHost;
    if (process.env.STAGE === &#39;dev&#39;) {
      deviceCheckHost = process.env.DEV_DEVICE_CHECK_API_URL;
    } else if (stage === &#39;prod&#39;) {
      deviceCheckHost = process.env.PROD_DEVICE_CHECK_API_URL;
    } else {
      util.cloudwatchLog(`--&gt; Unrecognized stage ${stage}. Aborting DC`);
      return;
    }

    // Make sure device is valid. If not, return false
    try {
      await deviceCheck.validateDevice(
            cert, keyId, teamId, deviceCheckToken, deviceCheckHost);
    } catch (err) {
      util.cloudwatchLog(`--&gt; DC validation failed. ${err}`);
      return false;
    }

    // Query for Device Check bits
    var dcQueryResults;
    try {
      dcQueryResults = await deviceCheck.queryTwoBits(
            cert, keyId, teamId, deviceCheckToken, deviceCheckHost);
    } catch (err) {
      dcQueryResults = null;
    }

    // If bit0 is true, then this device already has at least one row in the
    // search counts table
    if (dcQueryResults &amp;&amp; dcQueryResults.bit0) {

      // Try to get the counts row keyed on given user name
      const getParams = {
            TableName: process.env.SEARCH_COUNTS_TABLE,
            Key: { u: iCloudUserNameHash },
      };
      var countsRow;
      try {
            countsRow = await asyncAWS.invokeDynamoDBGet(dynamodb, getParams);
      } catch (err) {
            const msg = `--&gt; Couldn&#39;t get counts row during DC call: ${err}`;
            util.cloudwatchLog(msg);
            return false;
      }

      // If it doesn&#39;t exist, return false
      if (!countsRow) {
            return false;
      } else {// if it DOES exist, this is a legit request
            return true;
      }
    } else {

      // Initialize the row in memory
      const secsSinceEpoch = (new Date()).getTime() / 1000;
      const countsRow = {
            h: ,
            d: ,
            w: ,
            m: ,
            y: ,
            a: 0,
            u: iCloudUserNameHash,
      };

      // Put it in the search counts table
      const putParams = {
            Item: countsRow,
            TableName: process.env.SEARCH_COUNTS_TABLE,
      };
      try {
            await asyncAWS.invokeDynamoDBPut(dynamodb, putParams);
      } catch (err) {
            const msg = `--&gt; Couldn&#39;t set counts row in DC call: ${err}`
            util.cloudwatchLog(msg);
            return false;
      }

      // Set the device check bit
      try {
            await deviceCheck.updateTwoBits(true, false,
                cert, keyId, teamId, deviceCheckToken, deviceCheckHost);
      } catch (err) {
            const msg = `--&gt; DC update failed. ${iCloudUserNameHash} ${err}`;
            util.cloudwatchLog(msg);
            return false;
      }

      // If we got here, the request was legit
      return true;
    }
};

exports.main = async (event, context, callback) =&gt; {

    // Handle inputs
    const body = JSON.parse(event.body);
    const iCloudUserNameHash = body.iCloudUserNameHash;
    const deviceCheckToken = body.deviceCheckToken;
    const otherParams = body.otherParams;

    // If allowed to search, increment search counts then search
    var deviceCheckSucceeded;
    try {
      deviceCheckSucceeded =
            await isLegitDevice(deviceCheckToken, iCloudUserNameHash);
    } catch (err) {
      util.cloudwatchLog(`--&gt; Error checking device: ${err}`);
      return callback(null, resp.failure({}));
    }

    if (deviceCheckSucceeded) {

      // Do your stuff here

      return callback(null, resp.success({}));
    } else {
      return callback(null, resp.failure({}));
    }
};
</code></pre>

<p><strong>deviceCheck.js</strong></p>

<pre><code>const https = require(&#39;https&#39;);
const jwt = require(&#39;jsonwebtoken&#39;);
const uuidv4 = require(&#39;uuid/v4&#39;);

const util = require(&#39;../lib/util&#39;);

// Set the two Device Check bits for this device.
// Params:
//   bit0 (boolean) - true if never seen given iCloud user ID
//   bit1 (boolean) - TODO not used yet
//   cert (string) - Device Check certificate. Get from developer.apple.com)
//   keyId (string) - Part of metadata for Device Check certificate)
//   teamId (string) - My developer team ID. Can be found in iTunes Connect
//   dcToken (string) - Ephemeral Device Check token passed from frontend
//   deviceCheckHost (string) - API URL, which is either for dev or prod env
const updateTwoBits = async (
    bit0, bit1, cert, keyId, teamId, dcToken, deviceCheckHost) =&gt; {

    return new Promise((resolve, reject) =&gt; {
      var jwToken = jwt.sign({}, cert, {
            algorithm: &#39;ES256&#39;,
            keyid: keyId,
            issuer: teamId,
      });

      var postData = {
            &#39;device_token&#39; : dcToken,
            &#39;transaction_id&#39;: uuidv4(),
            &#39;timestamp&#39;: Date.now(),
            &#39;bit0&#39;: bit0,
            &#39;bit1&#39;: bit1,
      }

      var postOptions = {
            host: deviceCheckHost,
            port: &#39;443&#39;,
            path: &#39;/v1/update_two_bits&#39;,
            method: &#39;POST&#39;,
            headers: {
                &#39;Authorization&#39;: &#39;Bearer &#39; + jwToken,
            },
      };

      var postReq = https.request(postOptions, function(res) {
            res.setEncoding(&#39;utf8&#39;);

            var data = &#39;&#39;;
            res.on(&#39;data&#39;, function (chunk) {
                data += chunk;
            });

            res.on(&#39;end&#39;, function() {
                util.cloudwatchLog(
                  `--&gt; Update bits done with status code ${res.statusCode}`);
                resolve();
            });

            res.on(&#39;error&#39;, function(data) {
                util.cloudwatchLog(
                  `--&gt; Error ${res.statusCode} in update bits: ${data}`);
                reject();
            });
      });

      postReq.write(new Buffer.from(JSON.stringify(postData)));
      postReq.end();
    });
};

// Query the two Device Check bits for this device.
// Params:
//   cert (string) - Device Check certificate. Get from developer.apple.com)
//   keyId (string) - Part of metadata for Device Check certificate)
//   teamId (string) - My developer team ID. Can be found in iTunes Connect
//   dcToken (string) - Ephemeral Device Check token passed from frontend
//   deviceCheckHost (string) - API URL, which is either for dev or prod env
// Return:
//   { bit0 (boolean), bit1 (boolean), lastUpdated (String) }
const queryTwoBits = async (cert, keyId, teamId, dcToken, deviceCheckHost) =&gt; {

    return new Promise((resolve, reject) =&gt; {

      var jwToken = jwt.sign({}, cert, {
            algorithm: &#39;ES256&#39;,
            keyid: keyId,
            issuer: teamId,
      });

      var postData = {
            &#39;device_token&#39; : dcToken,
            &#39;transaction_id&#39;: uuidv4(),
            &#39;timestamp&#39;: Date.now(),
      }

      var postOptions = {
            host: deviceCheckHost,
            port: &#39;443&#39;,
            path: &#39;/v1/query_two_bits&#39;,
            method: &#39;POST&#39;,
            headers: {
                &#39;Authorization&#39;: &#39;Bearer &#39; + jwToken,
            },
      };

      var postReq = https.request(postOptions, function(res) {
            res.setEncoding(&#39;utf8&#39;);

            var data = &#39;&#39;;
            res.on(&#39;data&#39;, function (chunk) {
                data += chunk;
            });

            res.on(&#39;end&#39;, function() {
                try {
                  var json = JSON.parse(data);
                  resolve({
                        bit0: json.bit0,
                        bit1: json.bit1,
                        lastUpdated: json.last_update_time,
                  });
                } catch (e) {
                  const rc = res.statusCode;
                  util.cloudwatchLog(
                        `--&gt; DC query call failed. ${e}, ${data}, ${rc}`);
                  reject();
                }
            });

            res.on(&#39;error&#39;, function(data) {
                const code = res.statusCode;
                util.cloudwatchLog(
                  `--&gt; Error ${code} with query bits call: ${data}`);
                reject();
            });
      });

      postReq.write(new Buffer.from(JSON.stringify(postData)));
      postReq.end();
    });
};

// Make sure devie is valid.
// Params:
//   cert (string) - Device Check certificate. Get from developer.apple.com)
//   keyId (string) - Part of metadata for Device Check certificate)
//   teamId (string) - My developer team ID. Can be found in iTunes Connect
//   dcToken (string) - Ephemeral Device Check token passed from frontend
//   deviceCheckHost (string) - API URL, which is either for dev or prod env
const validateDevice = async (
    cert, keyId, teamId, dcToken, deviceCheckHost) =&gt; {

    return new Promise((resolve, reject) =&gt; {
      var jwToken = jwt.sign({}, cert, {
            algorithm: &#39;ES256&#39;,
            keyid: keyId,
            issuer: teamId,
      });

      var postData = {
            &#39;device_token&#39; : dcToken,
            &#39;transaction_id&#39;: uuidv4(),
            &#39;timestamp&#39;: Date.now(),
      }

      var postOptions = {
            host: deviceCheckHost,
            port: &#39;443&#39;,
            path: &#39;/v1/validate_device_token&#39;,
            method: &#39;POST&#39;,
            headers: {
                &#39;Authorization&#39;: &#39;Bearer &#39; + jwToken,
            },
      };

      var postReq = https.request(postOptions, function(res) {
            res.setEncoding(&#39;utf8&#39;);

            var data = &#39;&#39;;
            res.on(&#39;data&#39;, function (chunk) {
                data += chunk;
            });

            res.on(&#39;end&#39;, function() {
                util.cloudwatchLog(
                  `--&gt; DC validation done w/ status code ${res.statusCode}`);
                if (res.statusCode === 200) {
                  resolve();
                } else {
                  reject();
                }
            });

            res.on(&#39;error&#39;, function(data) {
                util.cloudwatchLog(
                  `--&gt; Error ${res.statusCode} in DC validate: ${data}`);
                reject();
            });
      });

      postReq.write(new Buffer.from(JSON.stringify(postData)));
      postReq.end();
    });
};

exports.updateTwoBits = updateTwoBits;
exports.queryTwoBits = queryTwoBits;
exports.validateDevice = validateDevice;
</code></pre> </li>
</ol></p>
                                   
                                                <p style="font-size: 20px;">关于ios - 如何针对 iCloud 验证 iCloud IDtoken ?,我们在Stack Overflow上找到一个类似的问题:
                                                        <a href="https://stackoverflow.com/questions/46820967/" rel="noreferrer noopener nofollow" style="color: red;">
                                                                https://stackoverflow.com/questions/46820967/
                                                        </a>
                                                </p>
                                       
页: [1]
查看完整版本: ios - 如何针对 iCloud 验证 iCloud ID token ?