An easy-to-use Node.js library for Signing in with Apple!
Now with support for fetching the name and email!
⚠️ Important note: Apple will only provide you with the name ONCE which is when the user taps "Sign in with Apple" on your app the first time. Keep in mind that you have to store this in your database at this time! For every login after that, Apple will provide you with a unique ID and the email that you can use to lookup the username in your database.
**Check out the passport version of this library here:**
https://github.com/ananay/passport-apple
https://npmjs.com/package/passport-apple
## Setup
Begin by installing the library:
```npm install apple-auth```
The configurations for Sign in with Apple are quite extensive so I've made an extensive SETUP.md file that you can read
https://github.com/ananay/apple-auth/blob/master/SETUP.md
## Example
I've created an example of how to use this library with Express! Check it out here:
https://github.com/ananay/apple-auth-example
**Example live on https://apple.ananay.dev**
## Usage
Initialize it using the following code:
```
const fs = require('fs');
const AppleAuth = require('apple-auth');
const config = fs.readFileSync("./config/config");
const auth = new AppleAuth(config, './config/AuthKey.p8');
```
Methods:
- ```auth.loginURL()``` - Creates the Login URL that your users will use to login to
- ```auth.accessToken(grantCode)``` - Gets the access token from the grant code received
- ```auth.refreshToken(refreshToken)``` - Gets the access token from a refresh token
## Troubleshooting
### `invalid_grant` when authorization code is generated by iOS App
Fix: If the authorizationCode was generated by your app, you should use your App ID as your clientId and not your service one. Discussion: https://github.com/ananay/apple-auth/issues/13
## Questions / Contributing
Feel free to open issues and pull requests. If you would like to be one of the core creators of this library, please reach out to me at i@ananayarora.com or message me on twitter @ananayarora!
================================================
FILE: SETUP.md
================================================
# Table of contents
- Apple Developer Account Configurations
- Create a new App ID
- Create a services ID
- Create a key
- Configuring the Library
# Apple Developer Account configurations
## Create a new App ID

You need to create this even if you don't have an iOS or a Mac app

Scroll down to "Capabilities", and find "Sign in with Apple" and check it.

Hit continue and then register.
## Create a services ID

Fill out the details here, and click configure on "Sign in with Apple".

Add your domain that you'll use in the "Domains" section and the redirect url that you want to allow

Click Continue and Register.
Now, you need to verify this domain and in order to do that, click on the Service ID that you just created, again, and click configure on "Sign in with Apple". When you do that, you should be able to see that there is a download and a verify button.

## Create a key
Go to the "Keys" section in your Developer account and create one like this:

Click on configure on the "Sign in with Apple" option and make sure it is assigned to the correct App ID. Click continue and register. Now, click on Download and *MAKE SURE YOU KEEP THE FILE SAFE AND SECURE! YOU CANNOT REDOWNLOAD IT ONCE YOU HAVE ALREADY DOWNLOADED IT*
# Configuring the library
Make a folder called "config" and add two files:
- The private key file that you just downloaded
- A new file called ```config.json```

Inside of config.json, paste the following sample:
```
{
"client_id": "",
"team_id": "",
"redirect_uri": "",
"key_id": "",
"scope": ""
}
```
The ```scope```field is to set what information we want to gather from the user. We can set ```email``` and/or ```name```. This information is still not provided by Apple because this feature is still in Beta - but if you provide it the first time, when Apple finally releases it, your application will already have this permission provided. Otherwise the user has to revoke permissions to your application and log in again to be prompted for the new information request.
The ```client_id``` is actually called the "Service ID" that you will create in the 'Identifiers' section

The ```team_id``` is the 10 character code on the top left of the developer page next to your name.

The ```redirect_uri``` is the return url you added in the developer portal

The ```key_id``` is the identifier for the private key you generated

You can now save this as config.json in the config folder.
# Customizing the library
When building this project, it may be useful to enable a debugging mode. Enabling this mode will log all failed requests to the console, and the reject error message will also contain more specific information about the error. To enable this mode, pass in an object with the key ```debug``` set to ```true``` when initializing the library. For example:
```
let auth = new AppleAuth(
config,
SECRET_KEY,
TEXT_METHOD,
{
debug: true
}
);
```
================================================
FILE: eslint.config.mjs
================================================
import js from "@eslint/js";
import globals from "globals";
import { defineConfig } from "eslint/config";
export default defineConfig([
{ files: ["**/*.{js,mjs,cjs}"], plugins: { js }, extends: ["js/recommended"] },
{ files: ["**/*.js"], languageOptions: { sourceType: "commonjs" } },
{ files: ["**/*.{js,mjs,cjs}"], languageOptions: { globals: globals.browser } },
]);
================================================
FILE: package.json
================================================
{
"name": "@zebedee/apple-auth",
"version": "1.1.0",
"description": "Sign in with Apple for NodeJS",
"main": "src/apple-auth.js",
"types": "src/apple-auth.d.ts",
"scripts": {
"dev": "nodemon src/apple-auth.js",
"start": "node src/apple-auth.js",
"lint": "eslint"
},
"repository": {
"type": "git",
"url": "git+https://github.com/ananay/apple-auth.git"
},
"keywords": [
"apple",
"sign",
"in",
"sso",
"auth",
"authentication"
],
"author": "Ananay Arora (https://ananayarora.com)",
"license": "MIT",
"dependencies": {
"axios": "^1.6.0",
"express": "^4.17.1",
"jsonwebtoken": "^9.0.0"
},
"bugs": {
"url": "https://github.com/ananay/apple-auth/issues"
},
"homepage": "https://github.com/ananay/apple-auth#readme",
"devDependencies": {
"@eslint/js": "^9.26.0",
"eslint": "^9.26.0",
"globals": "^16.1.0"
}
}
================================================
FILE: src/apple-auth.d.ts
================================================
declare module "apple-auth" {
export interface AppleAuthConfig {
client_id: string;
team_id: string;
redirect_uri: string;
key_id: string;
scope: string;
}
export interface AppleClientSecret{
_config: AppleAuthConfig,
_privateKeyLocation: string,
_privateKeyMethod: string
generate(): Promise;
}
// https://developer.apple.com/documentation/signinwithapplerestapi/tokenresponse
export interface AppleAuthAccessToken {
access_token: string;
expires_in: number;
id_token: string;
refresh_token: string;
token_type: "bearer";
}
// https://developer.apple.com/documentation/signinwithapplerestapi/errorresponse
export interface AppleAuthError {
error: "invalid_request" | "invalid_client" | "invalid_grant" | "unauthorized_client" | "unsupported_grant_type" | "invalid_scope";
}
export interface CustomConfig {
debug: boolean | undefined;
}
export default class AppleAuth {
constructor(config: AppleAuthConfig, privateKeyLocation: string, privateKeyMethod: string, customConfig?: CustomConfig)
public _state: string;
_tokenGenerator: AppleClientSecret;
loginURL(): string;
accessToken(code: string): Promise;
refreshToken(code: string): Promise;
revokeToken(unique_id: string): Promise;
}
}
================================================
FILE: src/apple-auth.js
================================================
/**
* Apple Auth Library that implements the 'Sign in with Apple' in NodeJS.
* Official Documentation: https://developer.apple.com/sign-in-with-apple/
* @author: Ananay Arora
*/
const axios = require('axios');
const AppleClientSecret = require("./token");
const crypto = require('crypto');
const qs = require('querystring');
const { Buffer } = require('node:buffer');
class AppleAuth {
/**
* Configure the parameters of the Apple Auth class
* @param {object} config Configuration options
* @param {string} config.client_id Client ID (also known as the Services ID
* in Apple's Developer Portal). Example: com.ananayarora.app
* @param {string} config.team_id Team ID for the Apple Developer Account
* found on top right corner of the developers page
* @param {string} config.redirect_uri The OAuth Redirect URI
* @param {string} config.key_id The identifier for the private key on the Apple
* @param {string} config.scope the scope of information you want to get from the user (user name and email)
* Developer Account page
* @param {string} privateKeyLocation Private Key Location / the key itself
* @param {string} privateKeyMethod Private Key Method (can be either 'file' or 'text')
* @param {object} customConfig Custom Configuration options
* @param {boolean} customConfig.debug Enable debug mode. This will print the verbose error messages returned by Apple's servers
*/
constructor(config, privateKey, privateKeyMethod, customConfig = {}) {
if (typeof config == 'object') {
if (Buffer.isBuffer(config)) {
this._config = JSON.parse(config.toString());
} else {
this._config = config;
}
} else {
this._config = JSON.parse(config);
}
if (typeof customConfig == 'object') {
if (Buffer.isBuffer(customConfig)) {
this._customConfig = JSON.parse(customConfig.toString());
} else {
this._customConfig = customConfig;
}
} else {
this._customConfig = JSON.parse(customConfig);
}
this._state = "";
this._tokenGenerator = new AppleClientSecret(this._config, privateKey, privateKeyMethod);
this.loginURL = this.loginURL.bind(this);
}
/**
* Return the state for the OAuth 2 process
* @returns {string} state –The state bytes in hex format
*/
get state() {
return this._state;
}
/**
* Generates the Login URL
* @returns {string} url –The Login URL
*/
loginURL() {
this._state = crypto.randomBytes(5).toString('hex');
return "https://appleid.apple.com/auth/authorize?" + qs.stringify({
response_type: "code id_token",
client_id: this._config.client_id,
redirect_uri: this._config.redirect_uri,
state: this._state,
scope: this._config.scope,
response_mode: "form_post"
});
}
/**
* Get the access token from the server
* based on the grant code
* @param {string} code
* @returns {Promise