Sign in with Apple & verify mobile app payload, under 5 minutes* for backend developers.

Syed Sirajul Islam Anik
10 min readOct 26, 2020

--

Image from: https://rb.gy/o7hivl

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.
Apple team id
  • 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.
select App IDs and press Continue

Continue with the App IDs selected and fill up the next page.

Registering the App ID

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.
select Service IDs and continue

Continue with the Service IDs selected and fill up the next page.

Fill up with reasonable details and follow given instructions

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

Edit your Service ID configuration

Click the “Sign In with Apple”, then click the Configure button.

Configure your service id

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.
Register your Key

Click the Register button and it’ll generate a new key for your application.

Apple key

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 — Mainly email and name 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 are code and id_token. You can request one or both. When requesting an id_token response type, response_mode must be either fragment or form_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 are name and email. You can request one, both, or none.
  • response_mode — Valid values are query, fragment and form_post. If you requested any scopes, the value must be form_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 the scope 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 is user_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 use firebase/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 the code in the response_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. ❤

--

--

Syed Sirajul Islam Anik

software engineer with "Senior" tag | procrastinator | programmer | !polyglot | What else 🙄 — Open to Remote