passport-apple

repository·master·Indexed 40 days ago

https://github.com/ananay/passport-apple

A Passport.js strategy for implementing 'Sign in with Apple' (version 2.0.2), supporting profile information fetching such as name and email. It includes the AppleStrategy constructor for authentication and the AppleClientSecret class for generating the required JSON Web Tokens (JWT) using Apple Developer credentials.

Tokens
2.9K
Snippets
5
Records
9
Agent score
50%

What's inside passport-apple

  1. Implement Apple authentication routes

    master

    Apple's authentication flow involves a POST request to your callback URL. Because browser SameSite policies often prevent cookies (and thus sessions) from being sent with external POST requests, you should redirect the POST request to a GET request using query parameters before passing it to Passport.

    // 1. Initial login request
    app.get('/apple', passport.authenticate('apple'));
    
    // 2. Handle the POST from Apple: Redirect POST to GET to preserve session/cookies
    app.post('/apple/callback', express.urlencoded({ extended: true }), (req, res) => {
      const { body } = req;
      const sp = new URLSearchParams();
      Object.entries(body).forEach(([key, value]) => sp.set(key, String(value)));
      res.redirect(`/v1/auth/apple/callback?${sp.toString()}`);
    });
    
    // 3. Handle the GET request (the actual Passport authentication)
    const failureRedirect = '/failure';
    
    app.get('/apple/callback', passport.authenticate('apple', {
      successReturnToOrRedirect: '/success',
      failureRedirect,
    }), (err, _req, res, _next) => {
      // Handle specific Apple errors
      if (err instanceof Error && (err.name === 'AuthorizationError' || err.name === 'TokenError')) {
        const sp = new URLSearchParams({ error: err.name });
        if ('code' in err && typeof err.code === 'string') sp.set('code', err.code);
        res.redirect(`${failureRedirect}${sp.toString()}`);
        return;
      }
    
      res.redirect(failureRedirect);
    });
  2. Install passport-apple

    master

    Install the package via npm or yarn. If you are using Express, you must also install and configure body-parser to handle URL-encoded bodies, as Apple sends data via POST requests.

    npm install --save passport-apple
    # Also required for Express users:
    npm install --save body-parser
    const bodyParser = require("body-parser");
    app.use(bodyParser.urlencoded({ extended: true }));
  3. Test Sign in with Apple locally

    master

    Apple requires HTTPS for redirect URLs. To test locally without setting up local HTTPS, use a service like redirectmeto.com.

    In your Apple Developer Console, set your callback URL to: https://redirectmeto.com/http://localhost:8080/auth/apple/callback (replacing 8080 with your local port).

    Warning: Remove this redirect URL from your Apple Developer configuration before deploying to production.

  4. Handle Apple authentication errors

    master

    When handling errors in the callback route, watch for these specific error types and codes:

    • AuthorizationError with code user_cancelled_authorize: Occurs when the user cancels the login process.
    • TokenError with code invalid_grant: Occurs when the authorization code has already been used.
  5. Initialize AppleStrategy

    master

    To use the strategy, pass a configuration object to AppleStrategy.

    Important Note on User Names: Apple only provides the user's name (via req.query.user) during the very first authentication attempt. You must capture and store this in your database immediately. Subsequent logins will only provide a unique ID (sub) and email.

    import AppleStrategy from 'passport-apple';
    import jsonwebtoken from 'jsonwebtoken';
    import assert from 'node:assert';
    
    passport.use(new AppleStrategy({
      clientID: '',
      teamID: '',
      callbackURL: 'https://redirectmeto.com/http://localhost:8080/auth/apple/callback',
      keyID: '',
      privateKeyString: '',
      passReqToCallback: true,
    }, async (req, accessToken, refreshToken, idToken, profile, cb) => {
      try {
        const decodedToken = jsonwebtoken.decode(idToken, { json: true });
        assert(decodedToken != null);
    
        // req.query['user'] contains firstName and lastName ONLY on the first auth
        const firstTimeUser = typeof req.query['user'] === 'string' ? JSON.parse(req.query['user']) : undefined;
    
        const { sub, email, email_verified } = decodedToken;
    
        // Implement your user lookup/creation logic here
        const dbUser = await upsertUser({ sub, email, email_verified, firstTimeUser });
    
        return cb(null, { id: dbUser.id });
      } catch (err) {
        return cb(err);
      }
    }));
  6. Configure AppleStrategy options

    master

    The AppleStrategy constructor accepts an options object with the following configuration keys:

    KeyTypeDescription
    clientIDstringThe Client ID (Services ID) from the Apple Developer Portal (e.g., com.example.app).
    teamIDstringThe Team ID for your Apple Developer Account.
    keyIDstringThe identifier for the private key on the Apple Developer Account page.
    callbackURLstringThe OAuth Redirect URI where Apple will send the user.
    privateKeyLocationstringThe file system path to your private key file.
    privateKeyStringstringThe raw string content of your private key.
    passReqToCallbackbooleanIf true, the req object is passed as the first argument to the verify callback. Defaults to true.
    authorizationURLstringThe Apple authorization URL. Defaults to https://appleid.apple.com/auth/authorize.
    tokenURLstringThe Apple token URL. Defaults to https://appleid.apple.com/auth/token.
  7. Generate Apple client secrets with AppleClientSecret

    master

    The AppleClientSecret class is used to generate the JSON Web Token (JWT) required for Apple authentication requests. It signs a token using your Apple Developer private key and specific configuration parameters.

    Configuration Object

    The constructor requires a config object with the following properties:

    • client_id: Your Apple Service ID.
    • team_id: Your Apple Developer Team ID.
    • redirect_uri: Your configured redirect URI.
    • key_id: The Key ID associated with your private key.

    Private Key Loading

    You can provide the private key in one of two ways:

    1. File Path: Pass a string representing the path to the .p8 file as the second argument (privateKeyLocation).
    2. String Content: Pass the raw private key string as the second argument (privateKeyLocation) if privateKeyLocation is falsy, or use the third argument (privateKeyString).

    Usage

    Call the .generate() method to receive a Promise that resolves to the signed JWT string. The token is configured to expire in 180 days (6 months).

    const AppleClientSecret = require('./path/to/token');
    
    const config = {
      client_id: 'com.example.service',
      team_id: 'TEAMID123',
      redirect_uri: 'https://example.com/callback',
      key_id: 'KEYID456'
    };
    
    // Option 1: Using a file path
    const generator = new AppleClientSecret(config, './AuthKey_ABC123.p8');
    
    // Option 2: Using a raw string
    // const generator = new AppleClientSecret(config, null, '-----BEGIN PRIVATE KEY-----\n...');
    
    generator.generate()
      .then(token => console.log('Generated Token:', token))
      .catch(err => console.error('Error:', err));
  8. Initialize AppleStrategy with Passport.js

    master

    To implement 'Sign in with Apple', use the AppleStrategy constructor. You must provide configuration options for your Apple Developer credentials and a verify callback function.

    Note that the idToken returned to the callback is an encoded string. To access user identity information (like sub), you should use a library like jsonwebtoken to decode it.

    In the verify callback, the fourth parameter (often represented as __) is required by the Passport implementation but currently contains no data because Apple does not yet pass profile data within the access token response.

    const AppleStrategy = require('passport-apple').Strategy;
    const passport = require('passport');
    
    passport.use(new AppleStrategy({
       clientID: "com.example.app", // Services ID
       teamID: "YOUR_TEAM_ID",
       callbackURL: "https://your-app.com/callback",
       keyID: "YOUR_KEY_ID",
       privateKeyLocation: "/path/to/private/key.p8",
       // OR use privateKeyString instead of privateKeyLocation
       passReqToCallback: true
    }, function(req, accessToken, refreshToken, idToken, __, cb) {
       // idToken is an encoded JWT. Use jsonwebtoken to decode it.
       // Example: const decoded = jwt.decode(idToken);
       // Use decoded.sub to identify the user.
       cb(null, idToken);
    }));
  9. Error messages for AppleClientSecret

    master

    When using AppleClientSecret, you may encounter the following error strings if operations fail:

    • AppleAuth Error - Couldn't read your Private Key file: <error>: Occurs if the provided privateKeyLocation path is invalid or unreadable.
    • AppleAuth Error – Error occurred while signing: <error>: Occurs if the jsonwebtoken signing process fails (e.g., due to an invalid key or incorrect algorithm).