Войти с помощью Apple (приложение iOS + проверка бэкэнд) API возвращает ошибку invalid_client

Я пытаюсь реализовать Вход через Apple с помощью приложения iOS и серверной части. Цель такая:

  1. Пользователь входит в приложение iOS.
  2. После положительного ответа приложение вызывает конечную точку на сервере и передает authorizationCode
  3. Теперь серверной части необходимо проверить authorizationCode другим вызовом сервера Apple.

Вот я запуталась. Чтобы сделать этот вызов, бэкэнд должен предоставить набор параметров:

URL

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

Я создал JWT для client_secret:

Свойства JWT

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"

Вчера я создал два ключа для двух приложений (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 и создал новый, и теперь он больше не работает с новым, но все еще со старым, хотя я удалил его на портале разработчиков. Я весьма озадачен.

Ошибка ответа:

{
    "error": "invalid_client"
}

Интересно, нужно ли Apple какое-то время на индексацию или что-то в этом роде. Я просто хочу понять, как это работает.


person Lukas Würzburger    schedule 05.09.2019    source источник
comment
какой идентификатор клиента вы использовали в запросе на проверку?   -  person stamaimer    schedule 08.09.2019
comment
@stamaimer - идентификатор пакета моего приложения для ios   -  person Lukas Würzburger    schedule 08.09.2019


Ответы (6)


Это могло произойти по нескольким причинам:

  1. client_id для Интернета должен быть Идентификатор службы. Для приложений это должен быть идентификатор набора приложений. (Даже если вы используете собственный диалог Apple, получите 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. Также важно указать заголовок User-Agent при вызове API токена. Также стоит упомянуть, что curl может вызывать эту ошибку, хотя она будет нормально работать, если вы вызовете ее с веб-сервера.
  2. Убедитесь, что вы устанавливаете правильный заголовок Content-Type: application/x-www-form-urlencoded вместо Content-Type: application/json, который некоторые библиотеки, такие как axios, устанавливают по умолчанию.
person Access Denied    schedule 09.12.2019
comment
У меня такая же проблема с invalid_client, какое значение вы указали для заголовка пользовательского агента? - person Philipp Jahoda; 27.01.2020
comment
@PhilippJahoda, вы можете поставить туда любое значение. У меня там есть название моего приложения. - person Access Denied; 27.01.2020
comment
Спасибо, до сих пор не могу заставить эту штуку работать. Я почти уверен, что мой токен (секрет) создан правильно, делали ли вы что-нибудь еще в консоли разработчика, кроме создания ключа для входа в Apple и привязки его к идентификатору пакета? - person Philipp Jahoda; 27.01.2020
comment
@PhilippJahoda не имел проблем с консолью. Я также сделал проверку домена, не совсем уверен, насколько это важно. - person Access Denied; 28.01.2020

Я немного ломал голову над тем, как проверить вход с помощью Apple из приложения iOS на стороне сервера, и не мог найти слишком много документации по этому поводу.

Я оставлю свою реализацию в nodeJS здесь, на случай, если она кому-то поможет; На самом деле я следовал подходу, изложенному здесь Кертис Герберт

  1. Из приложения iOS вы получаете ASAuthorizationAppleIDCredential, который включает, среди прочего, пользователя (id), электронную почту и identityToken (недолговечный JWT).
  2. На стороне сервера вы можете использовать веб-ключи Apple Json, доступные в https://appleid.apple.com/auth/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. Я использовал для этого библиотеку jose
    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 (identityToken), в котором вы можете это проверить;

  • iss - это https://appleid.apple.com
  • aud - идентификатор вашего пакета приложений
  • адрес электронной почты и подписка соответствуют вашим значениям ASAuthorizationAppleIDCredential
    {
            "iss": "https://appleid.apple.com",
            "aud": "BundleID",
            "sub": "credential.user",
            "email": "credential.email",
        }
person David Camargo    schedule 21.05.2020
comment
Замечательное объяснение! Просто вопрос @David: вы делаете эту проверку также для входа в систему или только для регистрации? - person Alex Bartiş; 22.05.2020
comment
Спасибо @ AlexBartiş, я все еще работаю над этим, но с этого момента я думаю, что буду обрабатывать токены доступа самостоятельно, чтобы мои пользователи вошли в систему. При этом я проверю сеанс Apple ID с помощью getCredentialState (forUserID : Завершение :) при каждом открытом приложении. Если они выйдут из системы и снова войдут в систему с Apple ID, я снова проверю identityToken. Что ты думаешь об этом? - person David Camargo; 23.05.2020
comment
да, проверка на стороне клиента на основе API Apple работает, но я также думаю о проверке токена идентификации при каждом входе в систему. Я считаю, что Apple предоставляет пользователю электронную почту только при первой регистрации. Я могу получить его из полезной нагрузки после проверки токена и отправить обратно при успешном входе в систему, чтобы мое устройство могло обновить его в своем хранилище. Если пользователь переустанавливает приложение, и я теряю электронное письмо с устройства, другого способа вернуть его нет. - person Alex Bartiş; 23.05.2020

Ваш собственный идентификатор приложения - это идентификатор пакета с префиксом идентификатора вашей команды, разделенным точкой.

«Apple App ID - это двухкомпонентная строка, используемая для идентификации одного или нескольких приложений. В частности, Apple app ID - это идентификатор вашей группы и идентификатор пакета, соединенный точкой, для example: 1A234H7ABC.com.yourdomain.YourApp."

Однако у меня такая же проблема, как заставить это работать.

person Nicholas Mordecai    schedule 10.09.2019
comment
Я использовал идентификатор пакета напрямую (без идентификатора команды) с некоторым успехом. Это из приложения, а не из Интернета. - person Chris Prince; 12.01.2020
comment
Идентификатор (App ID или Services ID) вашего приложения. Идентификатор не должен включать ваш Team ID, чтобы снизить риск раскрытия конфиденциальных данных конечному пользователю. Этот параметр требуется как для кода авторизации, так и для запросов проверки токена обновления. ref: developer.apple.com/documentation/sign_in_with_apple/ - person Dmitry; 29.06.2020

Мы получаем недействительный клиент, когда возникает проблема с вашим ключом или подпрограммой при проверке кода авторизации с помощью 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;
person n bajra    schedule 19.07.2020

Для людей, которые все еще борются с этим: Мне пришлось подождать 48 часов после создания моего первого ключа, чтобы его подхватили серверы Apple. Последние два дня я пробовал каждое решение, которое мог найти в сети, думая, что где-то ошибся. Нет .. что это исправлено, было пора (или кто-то в Apple что-то изменил на своей стороне). Я вызвал точно такой же запрос curl 10 минут назад и теперь получаю два разных ответа от API Apple. ТОЧНО такой же запрос (включая все параметры).

  • 10 минут назад: {error: invalid_client}
  • 1 минуту назад: {error: invalid_grant} (да !!!)
  • сейчас: успех (после обновления кода до нового кода авторизации, созданного моим приложением iOS)

Просто будьте терпеливы. Подождите 48 часов после создания первого ключа на странице https://developer.apple.com/account/resources/authkeys/list

person PanHrabia    schedule 12.03.2021

Если кто-то хочет использовать его в бэкэнде 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();
        }
    });
person z0mb1ek    schedule 18.03.2021