使用 Nextjs 并next-auth
进行所有身份验证。
我们已经成功集成了电子邮件(魔术链接)、Facebook 和 Google 身份验证,但出于某种原因,Apple 身份验证仍然是一个真正的 PITA。
我像往常一样设置了提供程序:
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,
}
},
}),
我有一个SignIn
回调准备好在成功验证后处理这些提供者中的每一个。
但是在成功验证后,它甚至没有得到我的回调,它在日志中显示以下错误:
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'
}
我尝试访问它输出的错误 URL(https://next-auth.js.org/errors#oauth_callback_error),但它根本没有帮助。
列入白名单的域和返回 URL 肯定都是正确的。对于 Google 和 Facebook,它们是相同的。
我最后的猜测是我产生了clientSecret
错误。所以我是这样做的:
我正在使用以下 Cli 脚本:
#!/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")))}`)
}
我已经package.json
为它设置了一个 Yarn 脚本,所以我可以这样称呼它:
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]
我完全忘记了我从哪里得到这个脚本。但是使用-h
标志运行它会给出它期望的所有参数以及为什么我使用上面的特定命令。