mirror of
https://github.com/better-auth/better-auth.git
synced 2026-10-19 11:24:57 -05:00
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:
committed by
Alex Yang
co-authored by
Copilot
parent
cb9e1bc1d1
commit
d364eff684
@@ -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",
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user