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.