fix: pass user field through idToken sign-in body for Apple name support (#8417)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Taesu
2026-03-06 10:11:06 -08:00
committed by Alex Yang
co-authored by Copilot
parent cb9e1bc1d1
commit d364eff684
4 changed files with 163 additions and 0 deletions
+1
View File
@@ -30,3 +30,4 @@ Agentic
Timestampz
Vercel
rgba
idtoken
@@ -113,6 +113,25 @@ const socialSignInBodySchema = z.object({
description: "Expiry date of the token",
})
.optional(),
/**
* The user object from the provider.
* This is only available for some providers like Apple.
*/
user: z
.object({
name: z
.object({
firstName: z.string().optional(),
lastName: z.string().optional(),
})
.optional(),
email: z.string().optional(),
})
.meta({
description:
"The user object from the provider. Only available for some providers like Apple.",
})
.optional(),
}),
),
scopes: z
@@ -257,6 +276,7 @@ export const signInSocial = <O extends BetterAuthOptions>() =>
idToken: token,
accessToken: c.body.idToken.accessToken,
refreshToken: c.body.idToken.refreshToken,
user: c.body.idToken.user,
});
if (!userInfo || !userInfo?.user) {
c.context.logger.error("Failed to get user info", {
@@ -3996,6 +3996,45 @@ exports[`open-api > should generate OpenAPI schema > openAPISchema 1`] = `
"description": "ID token from the provider",
"type": "string",
},
"user": {
"description": "The user object from the provider. Only available for some providers like Apple.",
"properties": {
"email": {
"description": undefined,
"type": [
"string",
"null",
],
},
"name": {
"description": undefined,
"properties": {
"firstName": {
"description": undefined,
"type": [
"string",
"null",
],
},
"lastName": {
"description": undefined,
"type": [
"string",
"null",
],
},
},
"type": [
"object",
"null",
],
},
},
"type": [
"object",
"null",
],
},
},
"required": [
"token",
+103
View File
@@ -1158,6 +1158,109 @@ describe("Apple Provider", async () => {
expect(session.data?.user.name).toBe("Better Auth");
});
it("should pass user name via idToken body for Apple sign-in", async () => {
const appleProfile = {
sub: "001341.example.idtoken",
email: "idtoken-user@privaterelay.appleid.com",
email_verified: true,
is_private_email: true,
real_user_status: 2,
/**
* Apple id_token JWT does not include the user's name.
* Name is available via the `user` field in the response.
* @see https://developer.apple.com/documentation/signinwithapplejs/authorizationi/id_token
* @see https://developer.apple.com/documentation/signinwithapple/incorporating-sign-in-with-apple-into-other-platforms#Handle-the-response
*/
};
const idToken = await signJWT(appleProfile, DEFAULT_SECRET);
const { client } = await getTestInstance(
{
socialProviders: {
apple: {
clientId: "test-apple-client",
clientSecret: "test-apple-secret",
verifyIdToken: async () => true,
},
},
},
{
disableTestUser: true,
},
);
const res = await client.signIn.social({
provider: "apple",
callbackURL: "/callback",
idToken: {
token: idToken,
user: {
name: {
firstName: "First",
lastName: "Last",
},
email: "idtoken-user@privaterelay.appleid.com",
},
},
});
expect(res.data).toBeDefined();
expect(res.data!.redirect).toBe(false);
const data = res.data as {
token: string;
user: { email: string; name: string };
};
expect(data.token).toBeDefined();
expect(data.user.email).toBe("idtoken-user@privaterelay.appleid.com");
expect(data.user.name).toBe("First Last");
});
it("should result in empty name when idToken body has no user field for Apple", async () => {
const appleProfile = {
sub: "001341.example.idtoken-noname",
email: "noname-user@privaterelay.appleid.com",
email_verified: true,
is_private_email: true,
real_user_status: 2,
};
const idToken = await signJWT(appleProfile, DEFAULT_SECRET);
const { client } = await getTestInstance(
{
socialProviders: {
apple: {
clientId: "test-apple-client",
clientSecret: "test-apple-secret",
verifyIdToken: async () => true,
},
},
},
{
disableTestUser: true,
},
);
const res = await client.signIn.social({
provider: "apple",
callbackURL: "/callback",
idToken: {
token: idToken,
},
});
expect(res.data).toBeDefined();
expect(res.data!.redirect).toBe(false);
const data = res.data as {
token: string;
user: { email: string; name: string };
};
expect(data.token).toBeDefined();
expect(data.user.email).toBe("noname-user@privaterelay.appleid.com");
expect(data.user.name).toBe("");
});
});
describe("Vercel Provider", async () => {