Repository: ananay/apple-auth Branch: master Commit: 207e5253508c Files: 9 Total size: 22.4 KB Directory structure: gitextract_tqs2csng/ ├── .gitignore ├── README.md ├── SETUP.md ├── eslint.config.mjs ├── package.json └── src/ ├── apple-auth.d.ts ├── apple-auth.js ├── apple-auth.js.flow └── token.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ node_modules .AppleDouble .DS_Store .npmrc ================================================ FILE: README.md ================================================ #  Sign in with Apple for Node.js Follow @ananayarora

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!

Created with ❤️ by Ananay Arora

================================================ 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 ![image](https://user-images.githubusercontent.com/5569219/59017558-6d643600-8861-11e9-927b-a4952b56f34e.png) You need to create this even if you don't have an iOS or a Mac app ![image](https://user-images.githubusercontent.com/5569219/59460984-f967f600-8e3d-11e9-926e-ef39aa1f8e48.png) Scroll down to "Capabilities", and find "Sign in with Apple" and check it. ![image](https://user-images.githubusercontent.com/5569219/59017720-dea3e900-8861-11e9-898e-f486c093edd8.png) Hit continue and then register. ## Create a services ID ![image](https://user-images.githubusercontent.com/5569219/59017808-16ab2c00-8862-11e9-8beb-4da7bb509b0c.png) Fill out the details here, and click configure on "Sign in with Apple". ![image](https://user-images.githubusercontent.com/5569219/59017915-5540e680-8862-11e9-8fd0-e26c425348db.png) Add your domain that you'll use in the "Domains" section and the redirect url that you want to allow ![image](https://user-images.githubusercontent.com/5569219/59018072-a7820780-8862-11e9-9e79-a8c7bb71ca45.png) 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. ![image](https://user-images.githubusercontent.com/5569219/59018636-f54b3f80-8863-11e9-919e-be685f171f95.png) ## Create a key Go to the "Keys" section in your Developer account and create one like this: ![image](https://user-images.githubusercontent.com/5569219/59018970-be295e00-8864-11e9-9129-3619ea3a5af3.png) 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``` ![image](https://user-images.githubusercontent.com/5569219/59019341-7e16ab00-8865-11e9-8408-958621d545b2.png) 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 ![image](https://user-images.githubusercontent.com/5569219/59019687-24fb4700-8866-11e9-8302-291a0d63006b.png) The ```team_id``` is the 10 character code on the top left of the developer page next to your name. ![image](https://user-images.githubusercontent.com/5569219/59019533-dcdc2480-8865-11e9-9db3-3e8f613a4f57.png) The ```redirect_uri``` is the return url you added in the developer portal ![image](https://user-images.githubusercontent.com/5569219/59018636-f54b3f80-8863-11e9-919e-be685f171f95.png) The ```key_id``` is the identifier for the private key you generated ![image](https://user-images.githubusercontent.com/5569219/59019916-87544780-8866-11e9-94d8-f454741dcbc6.png) 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} Access Token object */ accessToken(code) { return new Promise( (resolve, reject) => { this._tokenGenerator.generate().then((token) => { const payload = { grant_type: 'authorization_code', code, redirect_uri: this._config.redirect_uri, client_id: this._config.client_id, client_secret: token, }; axios({ method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded' }, data: qs.stringify(payload), url: 'https://appleid.apple.com/auth/token' }).then((response) => { resolve(response.data); }).catch((error) => { if (this._customConfig?.debug) { console.error(error); reject("AppleAuth Error - An error occurred while getting response from Apple's servers: " + error + " - " + error?.response?.data?.error_description); } // If customConfig.debug isn't set, output in this format. reject( `AppleAuth Error - An error occurred while getting response from Apple's servers: ${JSON.stringify(error)}` ); }); }).catch((err) => { reject(err); }); } ); } /** * Get the access token from the server * based on the refresh token * @param {string} refreshToken * @returns {object} Access Token object */ refreshToken(refreshToken) { return new Promise( (resolve, reject) => { this._tokenGenerator.generate().then((token) => { const payload = { grant_type: 'refresh_token', refresh_token: refreshToken, redirect_uri: this._config.redirect_uri, client_id: this._config.client_id, client_secret: token, }; axios({ method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded' }, data: qs.stringify(payload), url: 'https://appleid.apple.com/auth/token' }).then((response) => { resolve(response.data); }).catch((err) => { if(this._customConfig?.debug) { console.error(err); reject("AppleAuth Error - An error occurred while getting response from Apple's servers: " + err + " - " + err?.response?.data?.error_description); } reject("AppleAuth Error - An error occurred while getting response from Apple's servers: " + err); }); }).catch((err) => { reject(err); }); } ); } revokeToken(unique_id) { return new Promise( (resolve, reject) => { this._tokenGenerator.generate().then((token) => { const payload = { token: unique_id, redirect_uri: this._config.redirect_uri, client_id: this._config.client_id, client_secret: token, token_type_hint: 'access_token' }; axios({ method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded' }, data: qs.stringify(payload), url: 'https://appleid.apple.com/auth/revoke' }).then((response) => { resolve(response.data); }).catch((err) => { if(this._customConfig?.debug) { console.error(err); reject("AppleAuth Error - An error occurred while getting response from Apple's servers: " + err + " - " + err?.response?.data?.error_description); } reject("AppleAuth Error - An error occurred while getting response from Apple's servers: " + err); }); }).catch((err) => { reject(err); }); } ); } } module.exports = AppleAuth; ================================================ FILE: src/apple-auth.js.flow ================================================ export type AppleAuthConfig = {| client_id: string, team_id: string, redirect_uri: string, key_id: string, |} // https://developer.apple.com/documentation/signinwithapplerestapi/tokenresponse export type AppleAuthAccessToken = {| access_token: string, expires_in: number, id_token: string, refresh_token: string, token_type: "bearer", |} // https://developer.apple.com/documentation/signinwithapplerestapi/errorresponse export type AppleAuthError = {| error: "invalid_request" | "invalid_client" | "invalid_grant" | "unauthorized_client" | "unsupported_grant_type" | "invalid_scope", |} declare export default class AppleAuth { constructor(config: AppleAuthConfig, privateKeyLocation: string): AppleAuth; loginURL(): string; accessToken(code: string): Promise; refreshToken(code: string): Promise; } ================================================ FILE: src/token.js ================================================ /** * Generates a client secret for Apple auth * using the private key. * @author: Ananay Arora */ const jwt = require('jsonwebtoken'); const fs = require('fs'); class AppleClientSecret { /** * * @param {object} config * @param {string} config.client_id * @param {string} config.team_id * @param {string} config.redirect_uri * @param {string} config.key_id * @param {string} privateKeyLocation * @param {string} privateKeyMethod */ constructor(config, privateKeyLocation, privateKeyMethod) { this._config = config; this._privateKeyLocation = privateKeyLocation; if (typeof privateKeyMethod == 'undefined') { this._privateKeyMethod = 'file'; } else if (privateKeyMethod == 'text' || privateKeyMethod == 'file') { this._privateKeyMethod = privateKeyMethod; } else { this._privateKeyMethod = privateKeyMethod; } this.generate = this.generate.bind(this); this._generateToken = this._generateToken.bind(this); } /** * Generates the JWT token * @param {string} clientId * @param {string} teamId * @param {string} privateKey * @param {int} expiration * @returns {Promise} token */ _generateToken(clientId, teamId, privateKey, exp, keyid) { return new Promise( (resolve, reject) => { // Curate the claims const claims = { iss: teamId, iat: Math.floor(Date.now() / 1000), exp, aud: 'https://appleid.apple.com', sub: clientId, }; // Sign the claims using the private key jwt.sign(claims, privateKey, { algorithm: 'ES256', keyid }, (err, token) => { if (err) { reject("AppleAuth Error – Error occurred while signing: " + err); return; } resolve(token); }); } ); } /** * Reads the private key file calls * the token generation method * @returns {Promise} token - The generated client secret */ generate() { return new Promise( (resolve, reject) => { var that = this; function generateToken() { let exp = Math.floor(Date.now() / 1000) + ( 86400 * 180 ); // Make it expire within 6 months that._generateToken( that._config.client_id, that._config.team_id, that._privateKey, exp, that._config.key_id ).then((token) => { resolve(token); }).catch((err) => { reject(err); }); } if (!that._privateKey) { if (that._privateKeyMethod == 'file') { fs.readFile(that._privateKeyLocation, (err, privateKey) => { if (err) { reject("AppleAuth Error - Couldn't read your Private Key file: " + err); return; } that._privateKey = privateKey; generateToken(); }); } else { that._privateKey = that._privateKeyLocation; process.nextTick(generateToken); } } else { process.nextTick(generateToken); } } ); } } module.exports = AppleClientSecret;