Sign in with Apple (OAuth and OIDC)

You need a apple dev account. the client_id is the Apple Sign in Service ID.
Thankfully, Apple adopted the existing open standards OAuth 2.0 and OpenID Connect to use as the foundation for their new API. While they don’t explicitly call out OAuth or OIDC in their documentation, they use all the same terminology and API calls. This means if you’re familiar with these technologies, you should have no trouble using Sign in with Apple right away!
Let’s walk through building a short sample application that can leverage Apple’s new API to sign users in. You should be able to translate this code to your language of choice relatively easily once you see the requests being made.
The hardest part of this whole process is registering an application in the Apple Developer Portal. Typically, OAuth providers have a relatively straightforward process to register a new application which will give you a client ID and secret. But since Apple’s API is tied to their whole iOS app ecosystem, it’s a bit more complicated. They’ve also opted to use a public/private key client authentication method rather than a traditional client secret. But don’t worry, we’ll go through this step by step.
At the end of this process, you’ll end up with a registered client_id (which they call a Service ID), a private key downloaded as a file, and you’ll verify a domain and set up a redirect URL for the app.
Create an App ID
First, sign in to the Apple Developer Portal and click on Certificates, Identifiers and Profiles.
1589136093825.png

From the sidebar, choose Identifiers then click the blue plus icon.
1589136105859.png

Choose App IDs in this first step.
1589136114885.png

In the next screen, you’ll choose a description and Bundle ID for the App ID. The description isn’t too important, but type something descriptive. The Bundle ID is best when it’s a reverse-dns style string.
1589136130597.png

In this example, I’m using lol.avocado since the domain this app will be running on is avocado.lol.
You’ll also want to scroll down through the list of capabilities and check the box next to Sign In with Apple.
1589136145054.png

Go ahead and confirm this step.

Create a Services ID
The App ID in the previous step is a sort of way to collect things about this app. It seems redundant when you’re only making a simple web app login experience like this example, but it makes more sense once you have a native app and web app that you want to tie together under a single login experience.
The Services ID will identify the particular instance of your app, and is used as the OAuth client_id.
Go ahead and create a new identifier and choose Services IDs.
1589136208312.png

In the next step, you’ll define the name of the app that the user will see during the login flow, as well as define the identifier which becomes the OAuth client_id. Make sure to also check the Sign In with Apple checkbox.
1589136225444.png

You’ll also need to click the Configure button next to Sign In with Apple in this step. This is where you’ll define the domain your app is running on, as well as define the redirect URLs used during the OAuth flow.
1589136238938.png

Make sure your associated App ID is chosen as the Primary App ID. (If this is the first App ID you’ve made that uses Sign In with Apple, then it will probably already be selected.)
Enter the domain name your app will eventually be running at, and enter the redirect URL for your app as well. If you want to follow along with this blog post, then you can use the placeholder redirect URL https://example-app.com/connected_account.php which will catch the redirect so you can see the authorization code returned.
It’s worth noting that Apple doesn’t allow localhost URLs in this step, and if you enter an IP address like 127.0.0.1 it will fail later in the flow. You have to use a real domain here.
Go ahead and click Save and then Continue and Register until this step is all confirmed.
Ok! At this point, you now have an App ID container to hold everything, and you’ve created a Services ID which you’ll use as your OAuth client_id. The Identifier you entered for your Services ID is your OAuth client_id. In my example, that is lol.avocado.client.
Create a Private Key for Client Authentication
Rather than using simple strings as OAuth client secrets, Apple has decided to use a public/private key pair, where the client secret is actually a signed JWT. This next step involves registering a new private key with Apple.
Back in the main Certificates, Identifiers & Profiles screen, choose Keys from the side navigation.
1589136330244.png

Click the blue plus icon to register a new key. Give your key a name, and check the Sign In with Apple checkbox.
1589136340177.png

Click the Configure button and select your primary App ID you created earlier.
1589136353392.png

Apple will generate a new private key for you and let you download it only once. Make sure you save this file, because you won’t be able to get it back again later! The file you download will end in .p8, but it’s just text inside, so rename it to key.txt so that it’s easier to use in the next steps.
1589136368869.png

Lastly, go back and view the key information to find your Key ID which you’ll need in the next step.
1589136378286.png

Alright, that was a lot, but we are now ready to start writing some code! If you ran into any trouble, try checking out Apple’s documentation in case anything has changed since the publication of this blog post.

Generate the Client Secret
Rather than static client secrets, Apple requires that you derive a client secret yourself from your private key. 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.
The Ruby JWT library supports this algorithm, so we’ll use that to generate the secret.
First, make sure you’ve got Ruby installed, and then install the JWT gem by running this from the command line:
gem install jwt
Now the jwt gem should be available for use. Fill in the missing values at the top of this file, and save it as client_secret.rb. You should already have the client_id value from the previous step. You’ll also need your Apple Team ID. This is displayed in a few places, but the most convenient is in the top right corner of the screen. Use the Key ID you found in the previous step.
1589136431441.png

client_secret.rb
Ruby:
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
This code generates a JWT using the ES256 algorithm which includes a handful of claims. This JWT expires in 6 months, which is the maximum lifetime Apple will allow. If you’re generating a new client secret JWT every time a user authenticates, then you should use a much shorter expiration date, but this allows us to generate the secret once and use it in our sample apps easily.
Now you can run this from the command line and it will output a JWT.
ruby client_secret.rb
Code:
eyJraWQiOiJGUVVBN0RYUkJGIiwiYWxnIjoiRVMyNTYifQ.eyJpc3MiOiI3TUM2VVpSMlVWIiwiaWF0IjoxNTU5NjE0MjU2LCJleHAiOjE1NzUxNjYyNTYsImF1ZCI6Imh0dHBzOi8vYXBwbGVpZC5hcHBsZS5jb20iLCJzdWIiOiJsb2wuYXZvY2Fkby5jbGllbnQifQ.t6wIFrSKwuCZsJ9I1TWWBCdxmUMG3g0kNyNnxhkpG3oZAKY2UdXqL5CyRGTa21OYHa6ir1JFWkdBDjTNvt8hYC
This is described in Apple’s documentation Generate and validate tokens.
Or you can generate secret key with php:
PHP:
<?php
# composer require web-token/jwt-framework

require_once 'vendor/autoload.php';

use Jose\Component\Core\AlgorithmManager;
use Jose\Component\KeyManagement\JWKFactory;
use Jose\Component\Signature\Algorithm\ES256;
use Jose\Component\Signature\JWSBuilder;
use Jose\Component\Signature\Serializer\CompactSerializer;

/** Your team identifier: https://developer.apple.com/account/#/membership/ (Team ID) */
$teamId = '1A234BFK46';
/** The client id of your service: https://developer.apple.com/account/resources/identifiers/list/serviceId */
$clientId = 'org.example.service';
/** Code from request: https://appleid.apple.com/auth/authorize?response_type=code&client_id={$clientId}&scope=email%20name&response_mode=form_post&redirect_uri={$redirectUri} */
$code = 'ab1c23456fb104dbfa034e0e66bc58370.0.nrwxq.yQMut7nanacO82i7OvNoBg';
/** The ID of the key file: https://developer.apple.com/account/resources/authkeys/list (Key ID) */
$keyFileId = '1ABC6523AA';
/** The path of the file which you downloaded from https://developer.apple.com/account/resources/authkeys/list */
$keyFileName = 'AuthKey_1ABC6523AA.p8';
/** The redirect uri of your service which you used in the $code request */
$redirectUri = 'https://example.org';

$algorithmManager = new AlgorithmManager([new ES256()]);

$jwsBuilder = new JWSBuilder($algorithmManager);
$jws = $jwsBuilder
    ->create()
    ->withPayload(json_encode([
        'iat' => time(),
        'exp' => time() + 3600,
        'iss' => $teamId,
        'aud' => 'https://appleid.apple.com',
        'sub' => $clientId
    ]))
    ->addSignature(JWKFactory::createFromKeyFile($keyFileName), [
        'alg' => 'ES256',
        'kid' => $keyFileId
    ])
    ->build();

$serializer = new CompactSerializer();
$token = $serializer->serialize($jws, 0);

echo '<pre>';
var_dump($token);
echo '</pre>';

$data = [
    'client_id' => $clientId,
    'client_secret' => $token,
    'code' => $code,
    'grant_type' => 'authorization_code',
    'redirect_uri' => $redirectUri
];

$ch = curl_init();
curl_setopt_array ($ch, [
    CURLOPT_URL => 'https://appleid.apple.com/auth/token',
    CURLOPT_POSTFIELDS => http_build_query($data),
    CURLOPT_RETURNTRANSFER => true
]);
$response = curl_exec($ch);
curl_close ($ch);

var_export(json_decode($response, true));
/**
* array (
*   'access_token' => 'ab12cd3ef45db4f86a7d32cbbf7703a45.0.abcde.Ab01C3_D4elgkHOMcFuXpg',
*   'token_type' => 'Bearer',
*   'expires_in' => 3600,
*   'refresh_token' => 'abcdef12345678bb9bbbefba3e36118a2.0.mrwxq.Vo5t5ogmUXFERuNtiMbrvg',
*   'id_token' => 'RS256 Encoded Hash',
* )
*/
 
Last edited:
Back
Top