Sign in with Apple & verify mobile app payload, under 5 minutes* for backend developers.
Before writing this article, I went through my profile and found that I didn’t write any articles about JWT. JWT’s not mandatory here but could have self-promoted another article. But you can surely read my OAuth2.0 article. 😆
So, basically in OAuth2.0, the user authorizes third-party services to access their account for certain scopes that reside in the OAuth Service Provider. In this case Apple. That means, Apple has the user’s details and a known/unknown service (your service) will request on the user’s behalf to Apple and the user authorizes the request and tells Apple to give his data to your service.
As the article’s title mentions backend developers, thus it won’t bother about the frontend stuff. Like, how the button will be and everything. It’ll cover
- How to generate a request for the
code
- How to get user’s data from the
code
- How to validate the mobile application’s data in the backend
SO, LET’S START
Disclaimer: *
(asterisk) in the title means the time is a relative term. The article seems to be a long read. You’ll have to read fast and have a good internet connection. Even if you can’t make it within 5 minutes, I can’t be held responsible for this.
Setting things up
Before we move on, we need to set up a few things. Let’s start one after another
- Firstly, we need to know what is our Team ID. If you visit this page, you’ll be redirected to your membership account page. First and foremost, you’ll need to have a developer account with Apple. Write down the Team ID somewhere, we’ll need it later.
- Then you’ll need to go to this page and create an App ID unless you’ve already used one with “Sign In with Apple” before.
Continue with the App IDs selected and fill up the next page.
You must have to select the “Sign in with Apple” under the capabilities and make sure to keep it “Enable as a primary App ID” (which is the default). Now fill up the description and bundle id. After filling up the details, click Continue and then Register. Read the given instructions when filling up those details.
- After setting up the App ID, we’ll need to create a Service ID which acts as a Client ID in OAuth2.0.
Continue with the Service IDs selected and fill up the next page.
Click the Continue button and register your service id. You’ll be then redirected to the service ids page. If not, then you can go to the page by clicking the attached link above. Now, click on your newly created service id and you’ll be taken to the edit page. Edit your service id as per the following
Click the “Sign In with Apple”, then click the Configure button.
After clicking the “Configure” button, you’ll be shown a modal where you’ll need to register your domain/sub-domains with CSV (comma separated values), and then the callback/return URLs with CSV (comma separated values). Both domains and return URLs should support TLS. And yes, select the primary App ID you created earlier and select anything from the available list. If you’ve only one primary App ID, it’ll be selected by default. Then click next and confirm and continue. One thing I did, I skipped the “https://” in my domains field and appended “/” in my return URLs field. I didn’t experiment with these yet. But you may try doing that.
- When you’re done with all the above parts, you’ll then need to download a key file by generating one. You can go to this page and click on the PLUS (+) button beside the Keys label to create a new key for your app. Or you can directly visit this page to configure your key. When creating one, make sure to click the “Sign in with Apple” checkbox and click the Configure button. Select the Primary App ID (which you selected for creating the service id). Then click save and continue and then register the key.
Click the Register button and it’ll generate a new key for your application.
The red-colored text is the key id for your key, keep it somewhere. We’ll need it later. Now, either you can download the file now, or click done. But apple doesn’t let you download your key file a second time. Once you download, it’s your liability to keep it safe. And it cannot be shared and won’t be available to download next time.
Feeling bored? Odious? Frustrated? I had the same feeling. I was bored and was cursing them for what they did. But if you don’t know, your configuration is just done and a few miles yet to go. Hold on to your anger and curse for the next few minutes. Express once rather than in every step. 🤐 And Sia also said Never Give Up 😬
Now, let’s start the authorization part. First of all, we need to build an URL that will take the user to Apple’s authorization server and upon successfully authorizing our app, Apple will provide the user’s data to our application.
To create the URL, we’ll need to know a few values.
client_id
— The Service ID value, we create in the second step.redirect_uri
— Any of the redirect URLs value we registered in our second step by editing the service id.scope
— Mainlyemail
andname
fields. Choose whatever you need.
So, the URL will be like the following
https://appleid.apple.com/auth/authorize?
response_type=code
&response_mode=form_post
&client_id=your-service-id
&redirect_uri=url-encoded-redirected-uri
&state=optional-value
&scope=comma-separated-available-scopes
The above URL’s query params are written in each line for understanding the parameters. You’ll have to create a one-liner URL. Just remove the new lines.
response_type
— The type of response requested. Valid values arecode
andid_token
. You can request one or both. When requesting anid_token
response type,response_mode
must be eitherfragment
orform_post
client_id
— As specified above, it should be your service id. Don’t mix up with the App ID.redirect_uri
— As specified above, it should be the URL encoded value that you registered when editing your service id with the “Sign in with Apple” configuration.state
— Is an optional value. It can be the current state of the user before the authorization. You have to write the push and retrieval logic.scope
— It should be URL encoded space-separated values. The amount of user information requested from Apple. Valid values arename
andemail
. You can request one, both, or none.response_mode
— Valid values arequery
,fragment
andform_post
. If you requested any scopes, the value must beform_post
.
Check the documentation for further information.
When the user successfully authorizes the app, the next request is then forwarded to the redirect_uri
value based on the response_mode
's value given on the /auth/authorize
endpoint. We’ll discuss about the response_type
as code
and the response_mode
as the form_post
here. Check the documentation’s “Handle the Response” section for all the possible scenarios.
So, if you added “name” and “email” in the scope
parameter, then in the specified redirect_uri
endpoint, you’ll receive a POST request with the content-type of application/x-www-form-urlencoded
and the following data.
code
— A single-use authorization code that is valid for five minutes.id_token
— A JSON web token containing the user’s identity information.state
— The state contained in the Authorize URL.user
— A JSON string containing the data requested in thescope
property. The returned data is in the following format:{ "name": { "firstName": string, "lastName": string }, "email": string }
But if an error occurs, the HTTP body contains the following parameters:
error
— The returned error code. The only error code that might be returned isuser_cancelled_authorize
. This error code is returned if the user clicks Cancel during the web flow.state
— The state contained in the Authorize URL.
Now, when we have the code
in our form request, we can now proceed to the next endpoint to get the access & refresh token. To get the access token, refresh token and id token again in the backend, you’ll have to do the following. Generate a JWT token, with the following details.
- Header containing
{
"kid": "THE KEY ID WE GOT EARLIER IN THE ABOVE PROCESS",
"alg": "ES256"
}
- Payload/Body containing
{
"iss": "YOUR TEAM ID",
"iat": "should be less than or equal to now timestamp",
"exp": "should be less than or equal to +60days timestamp",
"aud": "https://appleid.apple.com",
"sub": "YOUR CLIENT/SERVICE ID"
}
- And create a JWT payload with these details signing with the key file (
.p8
) you’ve got. There is an available ruby code for this. Attached next to it. But if you’re a PHP developer like me, you can usefirebase/php-jwt
package like I did. Also attached below.
# client_secret.rb
# IDK ruby. I don't have it installed. I found it online.
# If you know ruby, or know the code, you're good to go.
# For PHP, check the following snippet.require 'jwt'
key_file = 'key.txt'
team_id = ''
client_id = ''
key_id = ''
ecdsa_key = OpenSSL::PKey::EC.new IO.read key_file
headers = {
'kid' => key_id
}
claims = {
'iss' => team_id,
'iat' => Time.now.to_i,
'exp' => Time.now.to_i + 86400*180,
'aud' => 'https://appleid.apple.com',
'sub' => client_id,
}
token = JWT.encode claims, ecdsa_key, 'ES256', headers
puts token
To generate the JWT with the help of PHP, you need to do the following.
<?php// require autoload fileuse Firebase\JWT\JWT;$teamId = 'YOUR_TEAM_ID';
$keyId = 'YOUR_KEY_ID';
$sub = 'YOUR_CLIENT_OR_SERVICE_ID';
$aud = 'https://appleid.apple.com'; // it's a fixed URL value
$iat = strtotime('now');
$exp = strtotime('+60days');// Check how to read the key file's content from
// https://stackoverflow.com/a/64478079/2190689
$keyContent = "THE .p8 FILE CONTENT";echo JWT::encode([
'iss' => $teamId,
'iat' => $iat,
'exp' => $exp,
'aud' => $aud,
'sub' => $sub,
], $keyContent, 'ES256', $keyId);// Write the snippet in a method, return the value from that method
// You can also cache the value and return instantly if it's a hit
// Or generate and save with a TTL for the miss.
By using this, you’ll get the client_secret
for the next request.
When we have the client_secret
then we can ask for the access & refresh token to the Apple’s server. The curl will be like below
curl -XPOST 'https://appleid.apple.com/auth/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=authorization_code' \
--data-urlencode 'redirect_uri=YOUR_REDIRECT_URI_W/O_URL_ENCODE' \
--data-urlencode 'client_id=YOUR_CLIENT_ID' \
--data-urlencode 'client_secret=VALUE_GENERATED_WITH_THE_SNIPPET' \
--data-urlencode 'code=RECEIVED_FROM_REDIRECT_URIS_FORM_REQUEST'
Catch is that the form should be submitted with application/x-www-form-urlencoded
.With successful credentials, you’ll receive a response like the following.
{
"access_token": "AN_ACCESS_TOKEN",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "REFRESH_TOKEN_OF_NO_USE",
"id_token": "A_JWT_TOKEN_CONTAINING_USER_INFORMATION"
}
Now, id_token
field is the source, where you’ll the values of the user. If you decode the id_token
values will be something like the following.
{
"iss": "https://appleid.apple.com",
"aud": "YOUR_CLIENT_ID",
"exp": TIMESTAMP_WHEN_THE_TOKEN_GETS_EXPIRED,
"iat": TIMESTAMP_WHEN_THE_TOKEN_WAS_GENERATED/ISSUED,
"sub": "AN_UNIQUE_IDENTIFIER_OF_USER_FOR_THE_CLIENT_ID",
"at_hash": "SOME_HASH_IDK",
"email": "THE_ACTUAL_OR_RELAY_EMAIL_OF_THE_USER",
"email_verified": "true",
"auth_time": TIMESTAMP_WHEN_THE_USER_AUTHORIZED_THE_APP,
"nonce_supported": BOOLEAN_STILL_IDK
}
Fetching the user object again
When you requested the /auth/token
endpoint with grant_type=authorization_code
you’re given a refresh_token
with the response. You can use that refresh_token
to fetch the user’s object once again. The following curl
request will help you fetching the user’s object.
curl -XPOST 'https://appleid.apple.com/auth/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=refresh_token' \
--data-urlencode 'client_id=YOUR_CLIENT_ID' \
--data-urlencode 'client_secret=GENERATED_CLIENT_SECRET' \
--data-urlencode 'refresh_token=VALUE_FROM_THE_PREVIOUS_REQUEST'
Here again, the form should be submitted with application/x-www-form-urlencoded
and the grant_type
is refresh_token
. We don’t need the redirect_url
data in this form. Again, you’ll get the same data format like the previous response.
Validating the fontend/mobile client input
To verify the frontend or mobile client input, you’ll have to get the id_token
from the client, and verify that the given JWT is valid.
You may want to know how the JWT’s are verified by checking out the answer. In Layman's Terms, Apple has a few private keys that are used to sign the JWT. And it provides a set of public keys that can be used to verify the JWT with the matching algorithm. You can check the available public keys by going here. In you programming language, you’ll find a few packages that will help you to generate JWKS to RSA. I used the previous PHP’s packages to generate the RSA from JWKS.
<?php// require autoload fileuse Firebase\JWT\JWK;
use Firebase\JWT\JWT;$kSet = []; //READ_FROM_APPLE_KEYSET_URL & CONVERT_TO_ARRAY
$token = "ID_TOKEN_VALUE_FROM_RESPONSE";
var_dump(JWT::decode($token, JWK::parseKeySet($kSet), [ 'RS256' ]));
Wrap the JWT::decode
within a try…catch block to handle the exceptions. If there are no exceptions, then the token is valid. And the client’s value can be trusted. This is how you can validate & verify the frontend/mobile client’s value.
Anomalies
I am sort of pissed off with Apple’s documentation.
- You cannot just request for
id_token
. You’ll always have to add thecode
in theresponse_type
. Check the question here. - You’ll be given the user’s name related values only in the
redirect_uri
‘s request. You cannot retrieve the name later on. If any malicious user spoofs the request, you’ll have to believe it blindly. - Again, the name is only given for the first time of the authorization. So, if the user previously authorized your app and a returning user (using the same service id for multiple applications) you’ll not get the user’s name the second time.
What else we can do? We have to follow how they implemented the thing. That’s it.
Anway, happy coding. ❤