Sign in with Apple (iOS App + Backend verification) API returns error "invalid_client"

13,169

Solution 1

There are several reasons why it could happen:

  1. client_id for web should be Service id. For apps it should be App bundle id. (Even if you use native apple dialog get code and later pass it to a webserver and then use it to request token API.) sub in JWT call should be the same as client_id. See the answer there Apple forum
  2. Your JWT library does not support encryption required by Apple sign-in. They use the JWT standard for this, using an elliptic curve algorithm with a P-256 curve and SHA256 hash. In other words, they use the ES256 JWT algorithm. Some JWT libraries don’t support elliptic curve methods, so make sure yours does before you start trying this out. ES256 and invalid_client
  3. Dates in token. Try to set the following
 expires: DateTime.UtcNow.AddDays(2), // expiry can be a maximum of 6 months
 issuedAt: DateTime.UtcNow.AddDays(-1),
 notBefore: DateTime.UtcNow.AddDays(-1),

It failed with invalid_client on my webserver, since Apple considered it as a certificate from the future, when I had:

 expires: DateTime.UtcNow.AddMinutes(5), // expiry can be a maximum of 6 months
 issuedAt: DateTime.UtcNow,
 notBefore: DateTime.UtcNow,
  1. It's also important to specify the User-Agent header when you call the token API. It's also worth mentioning that curl can throw this error, while it will work fine when you call it from a web server.
  2. Make sure you are setting the correct Content-Type: application/x-www-form-urlencoded header instead of Content-Type: application/json that some libraries like axios set by default.

Solution 2

I was scratching my head quite a bit on how to validate the Sign In with Apple from the iOS app on the server side and couldn't find too much documentation about it.

I'll leave my implementation in nodeJS here in case it helps anyone; I actually followed the approach outlined here by Curtis Herbert

  1. From the iOS app you get an ASAuthorizationAppleIDCredential which includes, among other details, a user (id), an email, and an identityToken (short lived JWT)
  2. On the server side you can use Apple Json Web Keys available in https://appleid.apple.com/auth/keys to generate a public key.
    {
      "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. Finally, using the public key you can verify the identityToken to make sure it was generated by Apple. I used the library jose for it
    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
                });

At the end you get a an object with the same structure as if you were decoding the JWT (identityToken) in which you can validate that;

  • iss is https://appleid.apple.com
  • aud is your app Bundle ID
  • email and sub match your ASAuthorizationAppleIDCredential values
    {
            "iss": "https://appleid.apple.com",
            "aud": "BundleID",
            "sub": "credential.user",
            "email": "credential.email",
        }

Solution 3

Your native APP ID is your bundle ID prefixed with your team ID, seperated by a dot.

"The Apple App ID is a two part string used to identify one or more apps. Specifically, the Apple app ID is your team ID and bundle ID joined with a period, for example: 1A234H7ABC.com.yourdomain.YourApp."

I'm having the same issue getting this to work however.

Share:
13,169

Related videos on Youtube

Lukas Würzburger
Author by

Lukas Würzburger

2022 - iOS, Server side Swift with Vapor 2021 - iOS, macOS (started SwiftUI) 2020 - iOS, macOS 2019 - iOS, macOS 2018 - iOS 2017 - iOS (started with Swift) 2016 - iOS 2015 - iOS 2014 - iOS 2013 - iOS 2012 - iOS, JSP 2011 - iOS, Web, JSP 2010 - iOS, Web 2009 - iPhoneOS, Web 2008 - Flex, iPhoneOS, Web, OSX (Objective-C) 2007 - Flash, Flex, Web, OSX (Objective-C) 2006 - Flash, Web 2005 - Flash, Web 2004 - Flash, Web

Updated on June 04, 2022

Comments

  • Lukas Würzburger
    Lukas Würzburger over 1 year

    I'm trying to implement Sign In with Apple with an iOS app and a backend. The goal is this:

    1. The User signs in on the iOS app
    2. After a positive response the app calls an endpoint on the backend and hands over the authorizationCode
    3. The backend now needs to verify the authorizationCode with another call to apple's server.

    Here I'm confused. In order to make this call, the backend needs to provide a bunch of parameters:

    URL

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

    Query Parameters

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

    I've generated a JWT for the client_secret:

    JWT Properties

    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"
    

    Yesterday I've created two keys for two apps (A and B) on the Dev Portal, used it to generate secrets and today app A worked and I've got a positive response:

    Positive response

    {
        "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, however, still doesn't work. Today I revoked the key for A and created a new one and now it doesn't work anymore with the new one, but still with the old one, even though I deleted it on the Dev Portal. I'm so confused.

    Response Error:

    {
        "error": "invalid_client"
    }
    

    I wonder if Apple needs some time indexing or something like that. I just want to understand how this works.

  • Chris Prince
    Chris Prince almost 4 years
    I've been using the bundle id directly (without the team Id) to some success. This is from an app, not from the web.
  • Philipp Jahoda
    Philipp Jahoda almost 4 years
    I'm having the same issue with invalid_client, what kind of value did you provide for the user agent header?
  • Access Denied
    Access Denied almost 4 years
    @PhilippJahoda you can put any value there. I have my app name there.
  • Philipp Jahoda
    Philipp Jahoda almost 4 years
    Thanks, still can't get this thing to work. I am pretty certain my token (secret) is created correctly, did you do anything else in the dev console other than creating the key for Apple Sign in and attaching it to a bundle identifier?
  • Access Denied
    Access Denied almost 4 years
    @PhilippJahoda didn't have problems with console. I also did domain verification, not quite sure if it's important.
  • trusk
    trusk over 3 years
    Awesome explanation! Just a question @David: do you do this check for login also, or just for registration?
  • David Camargo
    David Camargo over 3 years
    Thanks @AlexBartiş, I'm still working on this but I'm thinking, from that point onward, on handling access tokens myself to keep my users logged in. That being said, I'll check the Apple ID session with getCredentialState(forUserID:completion:) on every app open. If they logout and log back in with the Apple ID I'll check the identityToken again. What are your thoughts around this?
  • trusk
    trusk over 3 years
    yup, client side validation based on Apples API works, but I'm also thinking to the the identity token validation at each login also. My idea is that apple provides the user email only the first time the user registers. I can grab it from the payload after token validation and send it back on login successful so that my device can update it in it;s storage. If the user reinstalls the app and I lose the email from the device, there's no other way of getting it back.
  • Dmitry
    Dmitry over 3 years
    The identifier (App ID or Services ID) for your app. The identifier must not include your Team ID, to help mitigate sensitive data exposure to the end user. This parameter is required for both authorization code and refresh token validation requests. ref: developer.apple.com/documentation/sign_in_with_apple/…