18

我正在尝试使用 iOS 应用程序和后端实现Sign In with Apple 。目标是这样的:

  1. 用户在 iOS 应用上登录
  2. 在得到肯定响应后,应用程序调用后端的端点并移交authorizationCode
  3. 后端现在需要通过authorizationCode对苹果服务器的另一个调用来验证。

在这里我很困惑。为了进行这个调用,后端需要提供一堆参数:

网址

https://appleid.apple.com/auth/token

查询参数

client_id     = com.mycompany.appname
client_secret = ...
code          = ... // `authorizationCode` from the signin in the iOS app
grant_type    = authorization_code

JWTclient_secret

智威汤逊属性

header:
    kid: <key id, created on Apple Dev Portal>
claims:
    iss: <team id>
    iat: <current timestamp>
    exp: <current timestamp + 180 days>
    aud: "https://appleid.apple.com"
    sub: "com.mycompany.appname"

昨天我在 Dev Portal 上为两个应用程序(A 和 B)创建了两个密钥,用它来生成秘密,今天应用程序 A 工作了,我得到了积极的回应:

正面回应

{
    "access_token" : "a1e64327924yt49f5937d643e25a48b81.0.mxwz.GN9TjJIJ5_4dR6WjbZoVNw",
    "token_type" : "Bearer", 
    "expires_in" : 3600, 
    "refresh_token" : "rca76d9ebta644fde9edba269c61eeb41.0.mxwz.sMDUlXnnLLUOu2z0WlABoQ", 
    "id_token" : "eyJraWQiOiJBSURPUEsxIcccYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiZGUudHJ1ZmZscy5hcHBsZS1zaWduaW4tdGVzdCIsImV4cCI6MTU2NzcwMDI0MiwiaWF0IjoxNTY3Njk5NjQyLCJzdWaaaiIwMDA3NjkuYWY3NDdjMTlmZGRmNDJhNjhhYmFkZjhlNTQ1MmY3NjAuMjIwNSIsImF0X2hhc2giOiJrVThQTkZOUHYxS0RGUEtMT2hIY213IiwiYXV0aF90aW1lIjoxNTY3Njk5NjM5fQ.g3JD2MDGZ6wiVS9VMHpj24ER0XqJlunatmqpE7sRarMkhMHMTk7j8gty1lpqVBC6Z8L5CZuewdzLuJ5Odrd3_c1cX7gparTQE4jCyvyTACCPKHXReTC2hGRIEnAogcxv6HDWrtZgb3ENhoGhZW778d70DUdd-e4KKiAvzLOse-endHr51PaR1gv-cHPcwnm3NQZ144I-xhpU5TD9VQJ9IgLQvZGZ8fi8SOcu6rrk5ZOr0mpt0NbJNGYgH5-8iuSxo18QBWZDXoEGNsa4kS5GDkq5Cekxt7JsJFc_L1Np94giXhpbYHqhcO1pZSGFrJVaMvMMftZfuS_T3sh2yCqkcA"
}

但是,B 仍然不起作用。今天我撤销了 A 的密钥并创建了一个新的,现在它不再适用于新的,但仍然适用于旧的,即使我在 Dev Portal 上删除了它。我很混乱。

响应错误

{
    "error": "invalid_client"
}

我想知道苹果是否需要一些时间索引或类似的东西。我只是想了解这是如何工作的。

4

6 回答 6

9

发生这种情况的原因有几个:

  1. client_id对于web应该是Service id。对于应用程序,它应该是App bundle id。(即使您使用本机苹果对话框获取code并稍后将其传递给网络服务器,然后使用它来请求令牌 API。)sub在 JWT 调用中应该与client_id. 在 Apple 论坛上查看答案
  2. 您的 JWT 库不支持 Apple 登录所需的加密。他们为此使用 JWT 标准,使用带有 P-256 曲线和 SHA256 哈希的椭圆曲线算法。换句话说,他们使用 ES256 JWT 算法。一些 JWT 库不支持椭圆曲线方法,因此在开始尝试之前确保您的库支持。ES256 和 invalid_client
  3. 令牌中的日期。尝试设置以下
 expires: DateTime.UtcNow.AddDays(2), // expiry can be a maximum of 6 months
 issuedAt: DateTime.UtcNow.AddDays(-1),
 notBefore: DateTime.UtcNow.AddDays(-1),

它在我的网络服务器上出现 invalid_client 失败,因为 Apple 认为它是未来的证书,当时我有:

 expires: DateTime.UtcNow.AddMinutes(5), // expiry can be a maximum of 6 months
 issuedAt: DateTime.UtcNow,
 notBefore: DateTime.UtcNow,
  1. 在调用令牌 API 时指定User-Agent标头也很重要。还值得一提的是 curl 可以抛出这个错误,而当你从 web 服务器调用它时它会正常工作。
  2. 确保您设置了正确的Content-Type: application/x-www-form-urlencoded标头,而不是默认设置的Content-Type: application/json某些库(如axios)。
于 2019-12-09T04:44:03.680 回答
8

关于如何从服务器端的 iOS 应用程序验证使用 Apple 登录,我摸不着头脑,但找不到太多关于它的文档。

我将把我的实现留在 nodeJS 中,以防它帮助任何人;我实际上遵循了柯蒂斯赫伯特在这里概述的方法

  1. 从 iOS 应用程序中,您会获得一个 ASAuthorizationAppleIDCredential,其中包括用户 (id)、电子邮件和 identityToken(短期 JWT)等详细信息
  2. 在服务器端,您可以使用https://appleid.apple.com/auth/keys中提供的 Apple Json Web Keys来生成公钥。
    {
      "keys": [
        {
          "kty": "RSA",
          "kid": "86D88Kf",
          "use": "sig",
          "alg": "RS256",
          "n": "iGaLqP6y-SJCCBq5Hv6pGDbG_SQ11MNjH7rWHcCFYz4hGwHC4lcSurTlV8u3avoVNM8jXevG1Iu1SY11qInqUvjJur--hghr1b56OPJu6H1iKulSxGjEIyDP6c5BdE1uwprYyr4IO9th8fOwCPygjLFrh44XEGbDIFeImwvBAGOhmMB2AD1n1KviyNsH0bEB7phQtiLk-ILjv1bORSRl8AK677-1T8isGfHKXGZ_ZGtStDe7Lu0Ihp8zoUt59kx2o9uWpROkzF56ypresiIl4WprClRCjz8x6cPZXU2qNWhu71TQvUFwvIvbkE1oYaJMb0jcOTmBRZA2QuYw-zHLwQ",
          "e": "AQAB"
        },
        {
          "kty": "RSA",
          "kid": "eXaunmL",
          "use": "sig",
          "alg": "RS256",
          "n": "4dGQ7bQK8LgILOdLsYzfZjkEAoQeVC_aqyc8GC6RX7dq_KvRAQAWPvkam8VQv4GK5T4ogklEKEvj5ISBamdDNq1n52TpxQwI2EqxSk7I9fKPKhRt4F8-2yETlYvye-2s6NeWJim0KBtOVrk0gWvEDgd6WOqJl_yt5WBISvILNyVg1qAAM8JeX6dRPosahRVDjA52G2X-Tip84wqwyRpUlq2ybzcLh3zyhCitBOebiRWDQfG26EH9lTlJhll-p_Dg8vAXxJLIJ4SNLcqgFeZe4OfHLgdzMvxXZJnPp_VgmkcpUdRotazKZumj6dBPcXI_XID4Z4Z3OM1KrZPJNdUhxw",
          "e": "AQAB"
        }
      ]
    }
  1. 最后,您可以使用公钥验证 identityToken 以确保它是由 Apple 生成的。我为此使用了图书馆jos
    const axios = require('axios').default;
    const jose = require('jose')
    const {
      JWKS,  // JSON Web Key Set (JWKS)
      JWT,   // JSON Web Token (JWT)
      errors // errors utilized by jose
    } = jose    
    axios.get('https://appleid.apple.com/auth/keys')
                .then(function (response) {
                    // handle success
                    const key = jose.JWKS.asKeyStore(response.data);
                    const verified = jose.JWT.verify(identityToken, key);
                })
                .catch(function (error) {
                    // handle error
                    console.log(error);
                })
                .then(function () {
                    // always executed
                });

最后,您将获得一个与您正在解码 JWT(身份令牌)具有相同结构的对象,您可以在其中验证它;

  • iss 是https://appleid.apple.com
  • aud 是您的应用程序包 ID
  • email 和 sub 匹配您的 ASAuthorizationAppleIDCredential 值
    {
            "iss": "https://appleid.apple.com",
            "aud": "BundleID",
            "sub": "credential.user",
            "email": "credential.email",
        }
于 2020-05-21T08:53:58.887 回答
1

您的本机 APP ID 是您的捆绑包 ID,前缀为您的团队 ID,用点分隔。

“Apple App ID 是一个由两部分组成的字符串,用于标识一个或多个应用程序。具体而言,Apple 应用程序 ID 是您的团队 ID 和捆绑 ID 加上句点,例如:1A234H7ABC.com.yourdomain.YourApp。”

但是,我遇到了同样的问题。

于 2019-09-10T14:32:57.860 回答
0

当您的密钥有问题时,我们会得到“无效的客户端”,或者在使用 ES256 验证授权码时会得到“子”。使用下面的代码,您的问题将得到解决,因为我已经解决了我的问题。

代码 :

private static CngKey GetPrivateKey()
    {
        using (var reader = File.OpenText(ConfigurationManager.AppSettings["key2"].ToString()))  //put your key's path  like C:\ABC.p8
        {
            var ecPrivateKeyParameters = (ECPrivateKeyParameters)new PemReader(reader).ReadObject();
            var x = ecPrivateKeyParameters.Parameters.G.AffineXCoord.GetEncoded();
            var y = ecPrivateKeyParameters.Parameters.G.AffineYCoord.GetEncoded();
            var d = ecPrivateKeyParameters.D.ToByteArrayUnsigned();
            return EccKey.New(x, y, d);
        }
    }
   var utc0 = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);

    var issueTime = DateTime.Now;

    var iat = (int)issueTime.Subtract(utc0).TotalSeconds;
    var exp = (int)issueTime.AddMinutes(55).Subtract(utc0).TotalSeconds;
    var payload = new Dictionary<string, object>()
    {
        { "sub", "com.xyzttt" },  // you registered app 
        { "aud", "https://appleid.apple.com"},
        { "iss", "ABCDEFGHttt" },  //Team id 
        { "exp", exp },   }, //current time + @@@@ 
        { "iat", iat } } // current time
    };
    var extraHeader = new Dictionary<string, object>()
    {
          { "alg", "ES256" },
          { "kid", "5ABCDEFGH123tt21"}, //key id
    };
    var sb = GetPrivateKey(); 

    var ecdsa = new ECDsaCng(sb);

    byte[] headerBytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(extraHeader, Formatting.None));
    byte[] claimsBytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(payload, Formatting.None));


    var Rpayload = Base64UrlEncode(headerBytes) + "." + Base64UrlEncode(claimsBytes);
    var signature = ecdsa.SignData(Encoding.UTF8.GetBytes(Rpayload), HashAlgorithmName.SHA256);
    var data = Base64UrlEncode(signature);
    string token = Rpayload + "." + data;
   
    return token;
于 2020-07-19T15:39:46.460 回答
0

对于仍在为此苦苦挣扎的人:我必须在创建第一个密钥后等待 48 小时才能被 Apple 服务器接收。在过去的两天里,我一直在尝试我能在网上找到的每一个解决方案,以为我在某个地方犯了错误。不..什么是固定的时间(或者苹果公司的某个人最终修改了一些东西)。我在 10 分钟前和现在调用了完全相同的 curl 请求,从苹果的 api 获得了两个不同的响应。完全相同的请求(包括所有参数)。

  • 10 分钟前:{"error":"invalid_client" }
  • 1 分钟前:{"error":"invalid_grant"}(是的!!!)
  • 现在:成功(将“代码”更新为我的 iOS 应用程序生成的新授权代码后)

耐心一点。在https://developer.apple.com/account/resources/authkeys/list创建您的第一个密钥后等待 48 小时

于 2021-03-12T06:19:14.813 回答
0

如果有人想在 java 后端使用它,这里是 nimbus-jose-jwt java lib的有效代码

var publicKeys = JWKSet.load(new URL("https://appleid.apple.com/auth/keys"));

    publicKeys.getKeys().forEach(jwk -> {
        try {
            System.out.println(JWSObject.parse(appleIdentityToken)
                    .verify(new RSASSAVerifier(jwk.toRSAKey())));
        } catch (JOSEException | ParseException joseException) {
            joseException.printStackTrace();
        }
    });
于 2021-03-18T20:53:35.607 回答