From ad734552315e06a001e0913ff1ceebf42298a271 Mon Sep 17 00:00:00 2001 From: Taesu Date: Fri, 16 Oct 2026 03:35:55 +0900 Subject: [PATCH] fix(apple)!: align profile claims with identity tokens --- .changeset/apple-profile-types.md | 5 +++ .../core/src/social-providers/apple.test.ts | 34 +++++++++++++++++-- packages/core/src/social-providers/apple.ts | 8 ++--- 3 files changed, 41 insertions(+), 6 deletions(-) create mode 100644 .changeset/apple-profile-types.md diff --git a/.changeset/apple-profile-types.md b/.changeset/apple-profile-types.md new file mode 100644 index 0000000000..d471a3e965 --- /dev/null +++ b/.changeset/apple-profile-types.md @@ -0,0 +1,5 @@ +--- +"@better-auth/core": minor +--- + +Apple profile callbacks now accept unverified email claims and web sign-in profiles without native-only fields. Profile mappings must handle omitted private-email status, user status, and pictures. diff --git a/packages/core/src/social-providers/apple.test.ts b/packages/core/src/social-providers/apple.test.ts index 27619924bb..24f3965493 100644 --- a/packages/core/src/social-providers/apple.test.ts +++ b/packages/core/src/social-providers/apple.test.ts @@ -1,8 +1,8 @@ import { createHash } from "node:crypto"; import type { JWK } from "jose"; -import { exportJWK, generateKeyPair, SignJWT } from "jose"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { exportJWK, generateKeyPair, SignJWT, UnsecuredJWT } from "jose"; +import { assertType, beforeEach, describe, expect, it, vi } from "vitest"; import type { GenericEndpointContext } from "../types"; vi.mock("@better-fetch/fetch", () => ({ @@ -12,6 +12,7 @@ vi.mock("@better-fetch/fetch", () => ({ import { betterFetch } from "@better-fetch/fetch"; import { verifyProviderIdToken } from "../oauth2"; +import type { AppleProfile } from "./apple"; import { apple } from "./apple"; const mockedBetterFetch = vi.mocked(betterFetch); @@ -169,3 +170,32 @@ describe("apple id_token verification", () => { expect(seenPlatform).toBe("ios"); }); }); + +/** + * @see https://developer.apple.com/documentation/signinwithapplejs/authorizationi/id_token + */ +it.each([ + [true, true], + [false, false], + ["true", true], + ["false", false], +] as const)( + "accepts email_verified=%s without native-only claims", + async (emailVerified, expected) => { + const profile = { + sub: "apple-user-123", + name: "", + email_verified: emailVerified, + }; + assertType(profile); + assertType({ ...profile, is_private_email: "true" }); + const provider = apple({ clientId: "service.example.app" }); + + const result = await provider.getUserInfo({ + idToken: new UnsecuredJWT(profile).encode(), + }); + + expect(result?.user.emailVerified).toBe(expected); + expect(result?.data.name).toBe(""); + }, +); diff --git a/packages/core/src/social-providers/apple.ts b/packages/core/src/social-providers/apple.ts index aab88b6006..636b9e3af3 100644 --- a/packages/core/src/social-providers/apple.ts +++ b/packages/core/src/social-providers/apple.ts @@ -30,13 +30,13 @@ export interface AppleProfile { * Sign in with Apple at Work & School users, and this claim is "false" or * false for those users. */ - email_verified: true | "true"; + email_verified: boolean | "true" | "false"; /** * A string or Boolean value that indicates whether the email that the user * shares is the proxy address. The value can either be a string ("true" or * "false") or a Boolean (true or false). */ - is_private_email: boolean; + is_private_email?: boolean | "true" | "false"; /** * An Integer value that indicates whether the user appears to be a real * person. Use the value of this claim to mitigate fraud. The possible @@ -45,7 +45,7 @@ export interface AppleProfile { * in iOS 14 and later, macOS 11 and later, watchOS 7 and later, tvOS 14 * and later. The claim isn’t present or supported for web-based apps. */ - real_user_status: number; + real_user_status?: number; /** * The user’s full name in the format provided during the authorization * process. @@ -54,7 +54,7 @@ export interface AppleProfile { /** * The URL to the user's profile picture. */ - picture: string; + picture?: string; user?: AppleNonConformUser | undefined; }