From d364eff6847be3b04350df3a95dd4e1de2a3dab3 Mon Sep 17 00:00:00 2001 From: Taesu <166604494+bytaesu@users.noreply.github.com> Date: Fri, 6 Mar 2026 11:12:55 +0900 Subject: [PATCH] fix: pass `user` field through idToken sign-in body for Apple name support (#8417) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .cspell/auth-terms.txt | 1 + .../better-auth/src/api/routes/sign-in.ts | 20 ++++ .../__snapshots__/open-api.test.ts.snap | 39 +++++++ packages/better-auth/src/social.test.ts | 103 ++++++++++++++++++ 4 files changed, 163 insertions(+) diff --git a/.cspell/auth-terms.txt b/.cspell/auth-terms.txt index 754e7c95d0..2dbe8bbc5c 100644 --- a/.cspell/auth-terms.txt +++ b/.cspell/auth-terms.txt @@ -30,3 +30,4 @@ Agentic Timestampz Vercel rgba +idtoken diff --git a/packages/better-auth/src/api/routes/sign-in.ts b/packages/better-auth/src/api/routes/sign-in.ts index ab90e5243b..aa67adf72b 100644 --- a/packages/better-auth/src/api/routes/sign-in.ts +++ b/packages/better-auth/src/api/routes/sign-in.ts @@ -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 = () => 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", { diff --git a/packages/better-auth/src/plugins/open-api/__snapshots__/open-api.test.ts.snap b/packages/better-auth/src/plugins/open-api/__snapshots__/open-api.test.ts.snap index 205fa567be..c10e83b6b7 100644 --- a/packages/better-auth/src/plugins/open-api/__snapshots__/open-api.test.ts.snap +++ b/packages/better-auth/src/plugins/open-api/__snapshots__/open-api.test.ts.snap @@ -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", diff --git a/packages/better-auth/src/social.test.ts b/packages/better-auth/src/social.test.ts index 416d4d9e2e..10de6f2216 100644 --- a/packages/better-auth/src/social.test.ts +++ b/packages/better-auth/src/social.test.ts @@ -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 () => {