I'm trying to implement Sign In with Apple with an iOS app and a backend. The goal is this:
authorizationCode
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.
There are several reasons why it could happen:
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
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,
Content-Type: application/x-www-form-urlencoded
header instead of Content-Type: application/json
that some libraries like axios set by default.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
{
"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"
}
]
}
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": "https://appleid.apple.com",
"aud": "BundleID",
"sub": "credential.user",
"email": "credential.email",
}
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With