2

Using Nextjs and next-auth for everything authentication.

We've successfully integrated email (magic link), Facebook, and Google auth, but for some reason, Apple auth is still a real PITA.

I've set up the Provider, as usual:

AppleProvider({
      clientId: String(process.env.APPLE_ID),
      clientSecret: String(process.env.APPLE_SECRET),
      profile(profile) {
        return {
          id: profile.sub,
          name: profile.name,
          firstName: profile.name.split(' ').slice(0, -1).join(' '), // We assume the first name is everything before the last word in the full name
          lastName: profile.name.split(' ').slice(-1)[0], // We assume the last name is the last word in the full name
          email: profile.email,
          image: null,
        }
      },
    }),

I have a SignIn callback ready to handle each one of those providers upon a successful authentication.

But upon a successful authentication, it doesn't even get to my callback, it shows the following error in the logs:

https://next-auth.js.org/errors#oauth_callback_error invalid_client {
  error: {
    message: 'invalid_client',
    stack: 'OPError: invalid_client
' +
      '    at processResponse (/var/task/node_modules/openid-client/lib/helpers/process_response.js:45:13)
' +
      '    at Client.grant (/var/task/node_modules/openid-client/lib/client.js:1265:26)
' +
      '    at processTicksAndRejections (internal/process/task_queues.js:95:5)
' +
      '    at async Client.oauthCallback (/var/task/node_modules/openid-client/lib/client.js:561:24)
' +
      '    at async oAuthCallback (/var/task/node_modules/next-auth/core/lib/oauth/callback.js:114:16)
' +
      '    at async Object.callback (/var/task/node_modules/next-auth/core/routes/callback.js:50:11)
' +
      '    at async NextAuthHandler (/var/task/node_modules/next-auth/core/index.js:226:28)
' +
      '    at async NextAuthNextHandler (/var/task/node_modules/next-auth/next/index.js:16:19)
' +
      '    at async /var/task/node_modules/next-auth/next/index.js:52:32
' +
      '    at async Object.apiResolver (/var/task/node_modules/next/dist/server/api-utils.js:102:9)',
    name: 'OPError'
  },
  providerId: 'apple',
  message: 'invalid_client'
}

I tried visiting the error URL it's outputting (https://next-auth.js.org/errors#oauth_callback_error) but it wasn't helpful at all.

The whitelisted domains and return URLs are definitely all correct. They are the same for Google and Facebook.

My last guess is that I generated the clientSecret wrong. So here's how I did it:

I'm using the following Cli script:

#!/bin/node

import { SignJWT } from "jose"
import { createPrivateKey } from "crypto"

if (process.argv.includes("--help") || process.argv.includes("-h")) {
    console.log(`
  Creates a JWT from the components found at Apple.
  By default, the JWT has a 6 months expiry date.
  Read more: https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens#3262048
  Usage:
  node apple.mjs [--kid] [--iss] [--private_key] [--sub] [--expires_in] [--exp]
    
  Options:
    --help                 Print this help message
    --kid, --key_id        The key id of the private key
    --iss, --team_id       The Apple team ID
    --private_key          The private key to use to sign the JWT. (Starts with -----BEGIN PRIVATE KEY-----)
    --sub, --client_id     The client id to use in the JWT.
    --expires_in           Number of seconds from now when the JWT should expire. Defaults to 6 months.
    --exp                  Future date in seconds when the JWT expires
  `)
} else {
    const args = process.argv.slice(2).reduce((acc, arg, i) => {
        if (arg.match(/^--\w/)) {
            const key = arg.replace(/^--/, "").toLowerCase()
            acc[key] = process.argv[i + 3]
        }
        return acc
    }, {})

    const {
        team_id,
        iss = team_id,

        private_key,

        client_id,
        sub = client_id,

        key_id,
        kid = key_id,

        expires_in = 86400 * 180,
        exp = Math.ceil(Date.now() / 1000) + expires_in,
    } = args

    /**
     * How long is the secret valid in seconds.
     * @default 15780000
     */
    const expiresAt = Math.ceil(Date.now() / 1000) + expires_in
    const expirationTime = exp ?? expiresAt
    console.log(`
Apple client secret generated. Valid until: ${new Date(expirationTime * 1000)}
${await new SignJWT({})
            .setAudience("https://appleid.apple.com")
            .setIssuer(iss)
            .setIssuedAt()
            .setExpirationTime(expirationTime)
            .setSubject(sub)
            .setProtectedHeader({ alg: "ES256", kid })
            .sign(createPrivateKey(private_key.replace(/\\n/g, "\n")))}`)
}

I've set up a Yarn script in my package.json for it so I can call it this way:

yarn apple-gen-secret --kid [OUR-APPLE-KEY-ID] --iss [OUR-APPLE-TEAM-ID] --private_key "[OUR-APPLE-AUTH-KEY]" --sub [OUR-APPLE-SERVICE-ID]

I totally forgot where I got this script from. But running it with the -h flag gives all the parameters it expects and why I'm using the specific command above.

Nick Rameau
  • 1,258
  • 14
  • 23
  • I had the same issue. It turned out I had not generated the `client_secret` properly. The (JWT Debugger)[https://jwt.io/#debugger] is very useful to double check all the parameters are set up correctly. – Diana Vallverdu Jul 26 '22 at 15:11

0 Answers0