← All articles

Revoking Sign In with Apple Tokens from Capacitor in Node.js

Implement Apple token revocation in Node.js for App Store account-deletion requirements with Capacitor and Firebase Auth.

Published
Revoking Sign In with Apple Tokens from Capacitor in Node.js cover image

There was an announcement in May 2022 that account deletion requirements take effect June 30. I am not sure how enforcement worked (maybe there was a grace period), but from October 2022 my app was rejected for missing Sign In with Apple token revocation.

I use @capacitor-community/apple-sign-in with Firebase Authentication and wondered if the feature existed—it does not yet in that stack (earliest release might be Q1 2023).

https://github.com/firebase/firebase-ios-sdk/issues/9906#issuecomment-1285939323https://github.com/firebase/firebase-ios-sdk/issues/9906#issuecomment-1285939323

So I implemented it myself in Node.js and released it; here is a short summary of how.

Implementation

Get the Sign In with Apple authorization code

Revocation requires a token. Asking users to log in again at account deletion to "delete the token" feels suspicious when removing personal data, so first persist the authorization code you get on initial Sign In with Apple. When @capacitor-community/apple-sign-in succeeds, you receive a response object.

const appleLogin: {
  response: ResponseSignInWithApplePlugin;
} = await SignInWithApple.authorize();

The type looks like this.

export class ResponseSignInWithApplePlugin {
  user: string;
  identityToken: string;
  authorizationCode: string;

  email: string;
  givenName: string;
  familyName: string;
}

authorizationCode is the short-lived token. Send it to your server, obtain a refresh token, and keep it until the user deletes their account.

Obtain a refresh token

Get a Private Key from Apple

At https://developer.apple.com/account/resources/authkeys/list, create a Private Key.

Certificates, Identifiers & Profiles > Keys

Click the plus icon next to Keys and create a key. Enter any Key Name and enable Sign in with Apple. You will get three pieces of information.

  1. PrivateKey
    The secret key starting with -----BEGIN PRIVATE KEY----- in the downloaded file. You cannot download it again, so save it.

  2. Key ID and CONFIGURATION
    View these in View Key Details.

Key ID is as labeled. CONFIGURATION is the prefix before your bundle ID. My app bundle ID is jp.rdlabo.winecode, so it is the part before that (redacted in the image).

Create a JWT in Node

Authentication uses JWT, so install jsonwebtoken.

% npm install jsonwebtoken

Then create a method that builds the JWT (I use class-based NestJS, so this lives in a class).

import { sign } from 'jsonwebtoken';

const makeJWT = () => {
  //Sign with your team ID and key ID information.
  return sign(
      {
        iss: '●●●●', // Replace with your Apple Developer Team ID
        iat: Math.floor(Date.now() / 1000),
        exp: Math.floor(Date.now() / 1000) + 120,
        aud: 'https://appleid.apple.com',
        sub: 'jp.rdlabo.winecode',  // Replace with your bundle ID.
      },
      '●●●●', // Replace with the private key beginning with -----BEGIN PRIVATE KEY-----. A string is fine.
      {
        algorithm: 'ES256',
        header: {
          alg: 'ES256',
          kid: '●●●●', // Replace with the Key ID
        },
      },
  );
}

That gives you a JWT for Apple.

Use the JWT to obtain a refresh token

Apple's REST API expects form-urlencoded strings, so install qs to stringify objects easily.

% npm install qs

POST to Apple's REST API. Here I use @nestjs/axios, so it returns an Observable; a normal Promise-based HTTP client (or fetch) works too.

import { HttpService } from '@nestjs/axios';
import { stringify } from 'qs';

const { data } = await firstValueFrom(
  this.http.post(
    'https://appleid.apple.com/auth/token',
    stringify({
      code: authorizationCode,  // Authorization code obtained from Capacitor
      client_id: 'jp.rdlabo.winecode', // Your bundle ID
      client_secret: makeJWT(), // The method created earlier
      grant_type: 'authorization_code',
    }),
    {
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
      },
    },
  ),
)

On success, data contains a persisted token under the key refresh_token. Save it. With Firebase Authentication, store it in that service's user table; otherwise use your own persistent DB.

Revoke the user with the refresh token

Finally, account deletion—POST to Apple's REST API with the saved refresh token.

import { HttpService } from '@nestjs/axios';
import { stringify } from 'qs';

await firstValueFrom(
  this.http.post(
    'https://appleid.apple.com/auth/revoke',
    stringify({
      token: authorizationCode,  // Stored refresh token
      client_id: 'jp.rdlabo.winecode', // Your bundle ID
      client_secret: makeJWT(),
      token_type_hint: 'refresh_token',
    }),
    {
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
      },
    },
  ),
)

When the token is revoked, you cannot use it for API calls, and on iPhone you can confirm the app disappears from

Settings > Apple ID > Password & Security > Apps Using Apple ID

Summary

I have not heard many rejection stories for this requirement yet, so enforcement may still be ramping up. If you use Sign In with Apple with Capacitor and got rejected, I hope this helps.