Giter VIP home page Giter VIP logo

apple-signin-auth's Introduction

apple-signin-auth

 Apple signin for Node.js.

Follow @ahmad_tokyo

Prerequisites

  1. You should be enrolled in Apple Developer Program.
  2. Please have a look at Apple documentation related to "Sign in with Apple" feature.
  3. You should create App ID and Service ID in your Apple Developer Account.
  4. You should generate private key for your Service ID in your Apple Developer Account.

Apple Signin Setup

Deatiled confuguration instructions can be found at blog post and Apple docs.

Installation

npm install --save apple-signin-auth

OR

yarn add apple-signin-auth

Usage

1. Get authorization URL

Start "Sign in with Apple" flow by redirecting user to the authorization URL.

import appleSignin from 'apple-signin-auth';
// OR const appleSignin = require('apple-signin-auth');
// OR import { getAuthorizationUrl } from 'apple-signin-auth';

const options = {
  clientID: 'com.company.app', // Apple Client ID
  redirectUri: 'http://localhost:3000/auth/apple/callback',
  // OPTIONAL
  state: 'state', // optional, An unguessable random string. It is primarily used to protect against CSRF attacks.
  responseMode: 'query' | 'fragment' | 'form_post', // Force set to form_post if scope includes 'email'
  scope: 'email' // optional
};

const authorizationUrl = appleSignin.getAuthorizationUrl(options);

Alternatively, you can use Sign In with Apple browser javascript library.

2. Get access token

2.1. Retrieve "code" query param from URL string when user is redirected to your site after successful sign in with Apple. Example: http://localhost:3000/auth/apple/callback?code=somecode&state=123.

2.2. Exchange retrieved "code" to user's access token.

More detail can be found in Apple docs.

const clientSecret = appleSignin.getClientSecret({
  clientID: 'com.company.app', // Apple Client ID
  teamID: 'teamID', // Apple Developer Team ID.
  privateKey: 'PRIVATE_KEY_STRING', // private key associated with your client ID. -- Or provide a `privateKeyPath` property instead.
  keyIdentifier: 'XXX', // identifier of the private key.
  // OPTIONAL
  expAfter: 15777000, // Unix time in seconds after which to expire the clientSecret JWT. Default is now+5 minutes.
});

const options = {
  clientID: 'com.company.app', // Apple Client ID
  redirectUri: 'http://localhost:3000/auth/apple/callback', // use the same value which you passed to authorisation URL.
  clientSecret: clientSecret
};

try {
  const tokenResponse = await appleSignin.getAuthorizationToken(code, options);
} catch (err) {
  console.error(err);
}

Result of getAuthorizationToken command is a JSON object representing Apple's TokenResponse:

{
    access_token: 'ACCESS_TOKEN', // A token used to access allowed data.
    token_type: 'Bearer', // It will always be Bearer.
    expires_in: 300, // The amount of time, in seconds, before the access token expires.
    refresh_token: 'REFRESH_TOKEN', // used to regenerate new access tokens. Store this token securely on your server.
    id_token: 'ID_TOKEN' // A JSON Web Token that contains the user’s identity information.
}

3. Verify token signature and get unique user's identifier

try {
  const { sub: userAppleId } = await appleSignin.verifyIdToken(tokenResponse.id_token, {
    // Optional Options for further verification - Full list can be found here https://github.com/auth0/node-jsonwebtoken#jwtverifytoken-secretorpublickey-options-callback
    audience: 'com.company.app', // client id - can also be an array
    nonce: 'NONCE', // nonce // Check this note if coming from React Native AS RN automatically SHA256-hashes the nonce https://github.com/invertase/react-native-apple-authentication#nonce
    // If you want to handle expiration on your own, or if you want the expired tokens decoded
    ignoreExpiration: true, // default is false
  });
} catch (err) {
  // Token is not verified
  console.error(err);
}

4. Refresh access token after expiration

const clientSecret = appleSignin.getClientSecret({
  clientID: 'com.company.app', // Apple Client ID
  teamID: 'teamID', // Apple Developer Team ID.
  privateKey: 'PRIVATE_KEY_STRING', // private key associated with your client ID. -- Or provide a `privateKeyPath` property instead.
  keyIdentifier: 'XXXXXXXXXX', // identifier of the private key. - can be found here https://developer.apple.com/account/resources/authkeys/list
  // OPTIONAL
  expAfter: 15777000, // Duration after which to expire JWT
});

const options = {
  clientID: 'com.company.app', // Apple Client ID
  clientSecret
};

try {
  const {
    access_token
  } = appleSignin.refreshAuthorizationToken(refreshToken, options);
} catch (err) {
  console.error(err);
}

5. a, Revoke tokens with refresh_token

const clientSecret = appleSignin.getClientSecret({
  clientID: 'com.company.app', // Apple Client ID
  teamID: 'teamID', // Apple Developer Team ID.
  privateKey: 'PRIVATE_KEY_STRING', // private key associated with your client ID. -- Or provide a `privateKeyPath` property instead.
  keyIdentifier: 'XXXXXXXXXX', // identifier of the private key. - can be found here https://developer.apple.com/account/resources/authkeys/list
  // OPTIONAL
  expAfter: 15777000, // Duration after which to expire JWT
});

const options = {
  clientID: 'com.company.app', // Apple Client ID
  clientSecret,
  tokenTypeHint: 'refresh_token'
};

try {
  await appleSignin.revokeAuthorizationToken(refreshToken, options);
} catch (err) {
  console.error(err);
}

5. b, Revoke tokens with access_token

const clientSecret = appleSignin.getClientSecret({
  clientID: 'com.company.app', // Apple Client ID
  teamID: 'teamID', // Apple Developer Team ID.
  privateKey: 'PRIVATE_KEY_STRING', // private key associated with your client ID. -- Or provide a `privateKeyPath` property instead.
  keyIdentifier: 'XXXXXXXXXX', // identifier of the private key. - can be found here https://developer.apple.com/account/resources/authkeys/list
  // OPTIONAL
  expAfter: 15777000, // Duration after which to expire JWT
});

const options = {
  clientID: 'com.company.app', // Apple Client ID
  clientSecret,
  tokenTypeHint: 'access_token'
};

try {
  await appleSignin.revokeAuthorizationToken(accessToken, options);
} catch (err) {
  console.error(err);
}

Optional: Server-to-Server Notifications

Apple provides realtime server-to-server notifications of several user lifecycle events:

  • email-disabled: The user hides their email behind Apple's private email relay, and has opted to stop having emails forwarded by the private relay service.
  • email-enabled: The user hides their email behind Apple's private email relay, and has opted to resume having emails forwarded by the private relay service.
  • consent-revoked: The user has decided to stop using Apple ID with your application, e.g. by disconnecting the application from Settings. This should be treated as a sign-out out by the user.
  • account-delete: The user has asked Apple to permanently delete their Apple ID. The user identifier is no longer valid.

Notifications are sent for each app group.

The notification is sent as a POST request with a JSON body. The request body contains a JWT, with the event description on the JWT payload.

{
  "payload": "<server-to-server notification JWT>"
}

To receive these notifications, you must do the following steps.

1. Host the webhook

app.get("/apple-signin-webhook", async (req, res) => {
  try {
    const { events } = await appleSignin.verifyWebhookToken(
      req.body.payload,
      {
        // Optional Options for further verification - Full list can be found here https://github.com/auth0/node-jsonwebtoken#jwtverifytoken-secretorpublickey-options-callback
        audience: 'com.company.app', // client id - can also be an array
      },
    );
    const {
      sub: userAppleId,
      type,
      email // Only provided for email events
    } = events;

    switch (type) {
      case 'email-disabled':
        // Email will no longer be forwarded to the user via the private relay service
        break;
      case 'email-enabled':
        // Email will be forwarded to the user again
        break;
      case 'consent-revoked':
        // The user has decided to stop using Apple ID with this application - log them out
        break;
      case 'account-delete':
        // The user has deleted their Apple ID
        break;
    }

    res.sendStatus(200);
} catch (e) {
  // Event token is not verified
  console.error(err)
  res.sendStatus(500);
});

Note:

  • TLS 1.2 is required to receive notifications at the specified endpoint.

2. Configure the webhook URL in the Apple Developer console

2.1. Sign in to Apple Developer, go to "Certificates, Identifiers & Profiles", and select the Primary App ID for your application.

2.2 Enable the "Sign in with Apple" capability (if not already enabled) and click "Configure" (or "Edit").

2.3 Under "Server to Server Notification Endpoint", enter the fully-qualified URL for your webhook, e.g. https://example.com/api/apple-signin-webhook, and save the changes.

Notes:

  • A server-to-server webhook can only be configured for a Primary App ID.
  • The Apple docs for this step are located here.

Extra API functions

  • _setFetch: (fetchFn: function) => void - Sets the fetch function, defaults to node-fetch. eg: appleSigninAuth._setFetch(fetchWithProxy);

Extras

  • Handles apple public keys switching solving this issue https://forums.developer.apple.com/thread/129047
  • Caches Apple's public keys and only refetches when needed
  • ES6 (Can be imported using import appleSigning from 'apple-signin-auth/src')
  • Flow and TypeScript Types

Related Projects

Helpful resources

Contributing

Pull requests are highly appreciated! For major changes, please open an issue first to discuss what you would like to change.

apple-signin-auth's People

Contributors

a-tokyo avatar breyed avatar dauden avatar dependabot[bot] avatar ian-playside avatar nikonhub avatar nitinhsharma avatar ppeeou avatar pubkey avatar wsmd avatar zicyapp avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

apple-signin-auth's Issues

Sign in with apple for Electron

I've been stuck with the Electron puzzle for a long time now, I can't find anything, I have no experience with macos Native. I hope you can help me.
Hope to achieve the following effect.
image
Thank you very much for your reply.

Is this OAuth2 client?

Sign in with Apple is not following OAuth2 protocol? Can I just use OAuth2 package for Apple? I am so confusing why should I use this package.

require('apple-signin-auth') statement is not working and getting error is not a function

Below code sample is not working with the latest version of code it is not supporting require statement.

const appleSignin = require('apple-signin-auth');
const options = {..}
const authorizationUrl = appleSignin.getAuthorizationUrl(options);
console.log(authorizationUrl);

Error::
const authorizationUrl = appleSignin.getAuthorizationUrl(options);
^
TypeError: appleSignin.getAuthorizationUrl is not a function

can anyone please help me to get this shorted.

Looking forward to the solution.

Thanks,

Error getAuthorizationToken => invalid_client

Hello. Could someone explain me how getAuthorizationToken should work please ?
Because I get this error => 'invalid_client'

What I don't understand is why

const COMMON_HEADERS = {
  'Content-Type': 'application/json',
};

is used, meanwhile Apple says it should be (here)

form-data

The list of input parameters required for the server to validate the authorization code or refresh token.
Content-Type: application/x-www-form-urlencoded

So tweaking a little the code like this

const { URLSearchParams } = require('url');
 
const params = new URLSearchParams();
params.append('code', authorizationCode);
params.append('grant_type', 'authorization_code');
params.append('client_secret', clientSecret);
params.append('client_id', clientID);
 
fetch('https://appleid.apple.com/auth/token', { method: 'POST', body: params })
    .then(res => res.json())
    .then(json => console.log(json));

works fine, but not with application/json.

Anyone got it work as is ?

Structure changes

Hey @a-tokyo

I was thinking of restructuring the code by splitting the functionality into multiple files. feel free to delete/ignore/deny this suggestion

-src
    - utils
        - getAuthorizationUrl.js
        - getAuthorizationToken.js
        - refreshAuthorizationToken.js
        - verifyIdToken.js
        - operation.js
    - interface
        -  AppleIdTokenType.js
        -  AppleAuthorizationTokenResponseType.js
    - index.js 

index.js will responsible only for exporting the functionality.
operation.js will be responsible for common functions like _getApplePublicKeys, _setFetch.
remaining files are self-explanatory :)

FetchError: invalid json response body at https://appleid.apple.com/auth/keys reason: Unexpected token < in JSON at position 0.

Sometimes, I'm getting this error when trying fetch the public apple keys https://appleid.apple.com/auth/keys to verify a users token signature for Apple Signin:

FetchError: invalid json response body at https://appleid.apple.com/auth/keys reason: Unexpected token < in JSON at position 0." 

Reached out to apple directly too see if there could be an issue on there side of why valid JSON is not being sent back. https://developer.apple.com/forums/thread/656132. In Apple's docs, they specify to set the content type as so Content-Type: application/json.

One suggestion would be to add a header when fetching for the public keys. I tried pushing a branch but wasn't allow because of access rights. Happy to throw a PR up for this and also help maintain this library. First step would be too convert everything to typescript.

onst options = {
    headers: {
      'Content-Type': 'application/json',
    },
    method: 'GET',
  }

  // Fetch Apple's Public keys
  const data = await fetch(url.toString(), options)
    .then((res) => res.json());

Request

Hi,

I have this need: from my iOS application I need to login and get the token from lichess.org site without a webserver backend. Once the token has been obtained, I can use it in subsequent requests to access the site's features. Is this a library and a right method to get the token?

Thanks and regards,
Giordano

How to import this library to Ionic?

Hello,
thanks for library

I want to use this library in Ionic3, Angular based framework.

Steps:

  1. npm install --save apple-signin-auth
  2. import from node modules import appleSignIn from 'apple-signin-auth/lib';
  3. run code in component
const options = {
      clientID: 'myClientId',
      redirectUri: 'http://someUrl',
      state: 'state',
      responseMode: 'form_post',
      scope: 'name email'
    };
     

    let url = appleSignIn(options);
    console.warn(url);

and get error

ERROR TypeError: __WEBPACK_IMPORTED_MODULE_6_apple_signin_auth_lib___default(...) is not a function

image

getAuthorizationToken => { error: invalid_client }

Hi there, I just thought I'd let you know that I spent an entire day debugging this issue. To summarise, I fixed it by manually creating the clientSecret using jose, and ensuring { alg: 'es256' } in the header

On localhost, no issues were detected, but as soon as I deployed to production this call would fail. I'd seen some posts here and there about the es256 alg mentioned, and thought I'd try - even though I trust this library and thought it was a waste of time. I can't explain the localhost vs production issue, but fact is it now works, with the secret built as such:

const generateSecret = async () => {
    const jwt = new jose.SignJWT({
        'iat': Date.now() / 1000,
	'exp': Date.now() / 1000 + 3600,
	'iss': process.env.APPLE_TEAM_ID!,
	'aud': 'https://appleid.apple.com',
	'sub': process.env.APPLE_BUNDLE_ID!,
    });

    jwt.setProtectedHeader({alg: 'ES256', kid: process.env.APPLE_PRIVATE_KEY_ID!});

    const keyLike = await jose.importPKCS8(process.env.APPLE_PRIVATE_KEY!, 'es256');

    return jwt.sign(keyLike);
}

Hope this is helpful both to anyone facing the same issue right now, and to the authors here

Edit: Looking through the source, the process is identical besides using jose vs jsonwebtoken, and the optional exp field which I'm setting but left out when I used the library. So it's entirely possible something else is at play here, I don't want to point fingers. I mean to throw some light on my issue, which I think others might be facing too.

For reference, this is what I did before (on version ^1.7.5):

const client secret = appleSignin.getClientSecret({
        clientID: process.env.APPLE_BUNDLE_ID!, 
        teamID: process.env.APPLE_TEAM_ID!, 
        privateKey: process.env.APPLE_PRIVATE_KEY!, 
        keyIdentifier: process.env.APPLE_PRIVATE_KEY_ID!,
})

verifyIdToken invalid id token public key id ignoreExpiration

Hi!

I am having an issue with your library and I am not sure how to solve or what I am doing wrong. Therefore I thought I open an issue here. I hope this approach is ok with you.

I implemented my react native and node js authentication based on this tutorial:
https://rossbulat.medium.com/react-native-sign-in-with-apple-75733d3fbc3

I use your library in the backend, and I try to verify the IdToken with the ignoreExpiration flag set to true.

result = await appleSignin.verifyIdToken(token, { audience: config.CLIENT_ID_APPLE, ignoreExpiration: true, // ignore token expiry (never expires) });

But I get the following error:

JsonWebTokenError: error in secret or public key callback: input error: Invalid id token public key id at /app/node_modules/jsonwebtoken/verify.js:96:19 at _getIdTokenApplePublicKey (/app/node_modules/apple-signin-auth/lib/index.js:1:5730) at runMicrotasks () at processTicksAndRejections (internal/process/task_queues.js:95:5)

I also posted this question on SO: https://stackoverflow.com/questions/71570842/apple-auth-node-js-react-native-ignore-expiration-of-id-token-in-back-end

I am not sure what I am doing wrong here and any help is highly appreciated. @a-tokyo maybe you have a hint for me?

Thanks for the efforts!

accessToken is not valid.

Hi. The solution you created has helped me a lot.

await appleSignin provided by NPM apple-signin-auth
Succeeded in getting tokenResponse via .getAuthorizationToken() .

But the length of accessToken is too short.

The values I actually received are:

{
   "access_token": "ac6b736c7f2624fa1b9bea9987365d736.0.sysy.8f0JHW_a7jOrlPzCTYxIwQ",
   "token_type": "Bearer",
   "expires_in": 3600,
   "refresh_token": "rfe4bae11488c423d8010b12a7d581d5d.0.sysy.K1YJnRWIo64ghcuAjrZLow",
   "id_token": "ABCD...."
}

I checked https://jwt.io/ to validate the access_token, but the token is not valid.

can you give me some advice?

Proxy issue

Hey Team,

How do we use a proxy for this package, right now it is failing. should I change node-fetch with node-fetch-with-proxy and raise the request?

Support for validating Sign in with Apple server-to-server notifications

I've been adding support for Sign in with Apple server-to-server notifications to an app I work on. They're useful for handling a couple of user lifecycle edge cases (e.g. when a user revokes the login).

The notification is provided as a JWT, with the event data on the payload. The process of validating the JWT is identical to verifyIdToken, just with a different payload type (in fact, I'm currrently using verifyIdToken and then casting the payload to the appropriate type).

Unfortunately, Sign in with Apple server-to-server webhooks are extremely poorly documented - here's the only public info I've been able to find:

Is this something you'd be interested in adding support for? If so, I'd be happy to make a PR.

Redirect URI

Hi,

I wrote the other day asking if this software can be used by a mobile application without a backend web server. I understand that you still need to give a value to the "REDIRECT URI" variable. What value should this variable have if there is no backend web server? I'm in serious trouble, I can't go on.

Thanks for your help.
Giordano

FetchError: invalid json response body at https://appleid.apple.com/auth/revoke

as of version 1.7.1 when I'm trying to call revokeAuthorizationToken I got an issue. with the response body to JSON.

Error details:

FetchError: invalid json response body at https://appleid.apple.com/auth/revoke reason: Unexpected end of JSON input | CorrelationId: 507783AA-58EA-41EC-9984-0ABD52701AE3_1655970089157 FetchError: invalid json response body at https://appleid.apple.com/auth/revoke reason: Unexpected end of JSON input

I think it from here:

return fetch(url.toString(), { method: 'POST', body: params, }).then((res) => res.json());

We should check the 'res' is empty |null before convert to JSON.

revokeAuthorizationToken returns nothing

I'm using

const clientSecret = appleSignin.getClientSecret({
clientID: "com.company.app", // Apple Client ID
teamID: "teamID", // Apple Developer Team ID.
privateKey: "PRIVATE_KEY_STRING", // private key associated with your client ID. -- Or provide a "privateKeyPath" property instead.
keyIdentifier: "XXXXXXXXXX", // identifier of the private key. - can be found here https://developer.apple.com/account/resources/authkeys/list
// OPTIONAL
expAfter: 15777000, // Duration after which to expire JWT
});

const options = {
clientID: "com.company.app", // Apple Client ID
clientSecret
};

try {
  const response = appleSignin.revokeAuthorizationToken(refreshToken, options);
} catch (err) {
  console.error(err);
}

'response' I'm getting is always empty. I'm setting up 'refreshToken' as some dummy string and still no error. Please help.

Everything is set up correctly and I'm retrieving clientSecret correctly.

Typescript definitions?

It seems strange that this package is tagged with typescript here and on npm, but lacks typescript defintions.

Support revoking Access Tokens

Currently there is a support to only refresh tokens revoke, need to support revoke of access tokens (as here : https://developer.apple.com/documentation/sign_in_with_apple/revoke_tokens) this is to support Apple revoke tokens when deleting users (change which should be by 30 June 2022), more about that could be found here: https://developer.apple.com/support/offering-account-deletion-in-your-app.

If you approve my pull request I would add a new function (not to break the old one) which would enable revoking access tokens.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.