mirror of
https://github.com/better-auth/better-auth.git
synced 2026-05-28 01:46:45 -05:00
fix: correctly handle OAuth callback and Apple email field (#7181)
This commit is contained in:
@@ -68,7 +68,14 @@ export const callbackOAuth = createAuthEndpoint(
|
||||
throw c.redirect(`${defaultErrorURL}?error=invalid_callback_request`);
|
||||
}
|
||||
|
||||
const { code, error, state, error_description, device_id } = queryOrBody;
|
||||
const {
|
||||
code,
|
||||
error,
|
||||
state,
|
||||
error_description,
|
||||
device_id,
|
||||
user: userData,
|
||||
} = queryOrBody;
|
||||
|
||||
if (!state) {
|
||||
c.context.logger.error("State not found", error);
|
||||
@@ -131,10 +138,24 @@ export const callbackOAuth = createAuthEndpoint(
|
||||
c.context.logger.error("", e);
|
||||
throw redirectOnError("invalid_code");
|
||||
}
|
||||
const parsedUserData = userData
|
||||
? safeJSONParse<{
|
||||
name?: {
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
};
|
||||
email?: string;
|
||||
}>(userData)
|
||||
: null;
|
||||
|
||||
const userInfo = await provider
|
||||
.getUserInfo({
|
||||
...tokens,
|
||||
user: c.body?.user ? safeJSONParse<any>(c.body.user) : undefined,
|
||||
/**
|
||||
* The user object from the provider
|
||||
* This is only available for some providers like Apple
|
||||
*/
|
||||
user: parsedUserData ?? undefined,
|
||||
})
|
||||
.then((res) => res?.user);
|
||||
|
||||
|
||||
@@ -954,6 +954,178 @@ describe("updateAccountOnSignIn", async () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Apple Provider", async () => {
|
||||
it("should not use email as fallback for name when name is not provided", async () => {
|
||||
const appleProfile = {
|
||||
sub: "001341.example.1128",
|
||||
email: "user@privaterelay.appleid.com",
|
||||
email_verified: true,
|
||||
is_private_email: true,
|
||||
real_user_status: 2,
|
||||
// No name field
|
||||
};
|
||||
|
||||
mswServer.use(
|
||||
http.post("https://appleid.apple.com/auth/token", async () => {
|
||||
const idToken = await signJWT(appleProfile, DEFAULT_SECRET);
|
||||
return HttpResponse.json({
|
||||
access_token: "apple_access_token",
|
||||
id_token: idToken,
|
||||
token_type: "Bearer",
|
||||
expires_in: 3600,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
const { client, cookieSetter } = await getTestInstance(
|
||||
{
|
||||
socialProviders: {
|
||||
apple: {
|
||||
clientId: "test-apple-client",
|
||||
clientSecret: "test-apple-secret",
|
||||
// Disable ID token verification for testing
|
||||
verifyIdToken: async () => true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
disableTestUser: true,
|
||||
},
|
||||
);
|
||||
|
||||
const headers = new Headers();
|
||||
const signInRes = await client.signIn.social({
|
||||
provider: "apple",
|
||||
callbackURL: "/callback",
|
||||
fetchOptions: {
|
||||
onSuccess: cookieSetter(headers),
|
||||
},
|
||||
});
|
||||
|
||||
const state = new URL(signInRes.data!.url!).searchParams.get("state") || "";
|
||||
|
||||
await client.$fetch("/callback/apple", {
|
||||
query: {
|
||||
state,
|
||||
code: "apple_test_code",
|
||||
},
|
||||
headers,
|
||||
method: "GET",
|
||||
onError(context) {
|
||||
cookieSetter(headers)(context as any);
|
||||
},
|
||||
});
|
||||
|
||||
const session = await client.getSession({
|
||||
fetchOptions: {
|
||||
headers,
|
||||
},
|
||||
});
|
||||
|
||||
// Name should NOT be the email address
|
||||
expect(session.data?.user.name).not.toBe("user@privaterelay.appleid.com");
|
||||
// Name should be undefined, null, or space when not provided
|
||||
expect(
|
||||
session.data?.user.name === undefined ||
|
||||
session.data?.user.name === null ||
|
||||
session.data?.user.name === " ",
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("should use firstName and lastName when provided in token.user", async () => {
|
||||
const appleProfile = {
|
||||
sub: "001341.example.1129",
|
||||
email: "user2@privaterelay.appleid.com",
|
||||
email_verified: true,
|
||||
is_private_email: true,
|
||||
real_user_status: 2,
|
||||
};
|
||||
|
||||
mswServer.use(
|
||||
http.post("https://appleid.apple.com/auth/token", async () => {
|
||||
const idToken = await signJWT(appleProfile, DEFAULT_SECRET);
|
||||
return HttpResponse.json({
|
||||
access_token: "apple_access_token",
|
||||
id_token: idToken,
|
||||
token_type: "Bearer",
|
||||
expires_in: 3600,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
const { client, cookieSetter } = await getTestInstance(
|
||||
{
|
||||
socialProviders: {
|
||||
apple: {
|
||||
clientId: "test-apple-client",
|
||||
clientSecret: "test-apple-secret",
|
||||
verifyIdToken: async () => true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
disableTestUser: true,
|
||||
},
|
||||
);
|
||||
|
||||
const headers = new Headers();
|
||||
const signInRes = await client.signIn.social({
|
||||
provider: "apple",
|
||||
callbackURL: "/callback",
|
||||
fetchOptions: {
|
||||
onSuccess: cookieSetter(headers),
|
||||
},
|
||||
});
|
||||
|
||||
const state = new URL(signInRes.data!.url!).searchParams.get("state") || "";
|
||||
const userData = JSON.stringify({
|
||||
name: {
|
||||
firstName: "Better",
|
||||
lastName: "Auth",
|
||||
},
|
||||
email: "user2@privaterelay.appleid.com",
|
||||
});
|
||||
|
||||
let redirectLocation: string | null = null;
|
||||
|
||||
await client.$fetch("/callback/apple", {
|
||||
body: {
|
||||
state,
|
||||
code: "apple_test_code",
|
||||
user: userData,
|
||||
},
|
||||
headers,
|
||||
method: "POST",
|
||||
onError(context) {
|
||||
// Expecting 302 redirect
|
||||
expect(context.response.status).toBe(302);
|
||||
redirectLocation = context.response.headers.get("location");
|
||||
expect(redirectLocation).toBeDefined();
|
||||
expect(redirectLocation).toContain("/callback/apple");
|
||||
expect(redirectLocation).toContain("user=");
|
||||
},
|
||||
});
|
||||
|
||||
const redirectUrl = new URL(redirectLocation!);
|
||||
await client.$fetch("/callback/apple", {
|
||||
query: Object.fromEntries(redirectUrl.searchParams),
|
||||
headers,
|
||||
method: "GET",
|
||||
onError(context) {
|
||||
cookieSetter(headers)(context as any);
|
||||
},
|
||||
});
|
||||
|
||||
const session = await client.getSession({
|
||||
fetchOptions: {
|
||||
headers,
|
||||
},
|
||||
});
|
||||
|
||||
expect(session.data?.user.name).toBe("Better Auth");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Vercel Provider", async () => {
|
||||
beforeAll(async () => {
|
||||
mswServer.use(
|
||||
|
||||
@@ -161,9 +161,18 @@ export const apple = (options: AppleOptions) => {
|
||||
if (!profile) {
|
||||
return null;
|
||||
}
|
||||
const name = token.user
|
||||
? `${token.user.name?.firstName} ${token.user.name?.lastName}`
|
||||
: profile.name || profile.email;
|
||||
|
||||
// TODO: " " masking will be removed when the name field is made optional
|
||||
let name: string;
|
||||
if (token.user?.name) {
|
||||
const firstName = token.user.name.firstName || "";
|
||||
const lastName = token.user.name.lastName || "";
|
||||
const fullName = `${firstName} ${lastName}`.trim();
|
||||
name = fullName || " ";
|
||||
} else {
|
||||
name = profile.name || " ";
|
||||
}
|
||||
|
||||
const emailVerified =
|
||||
typeof profile.email_verified === "boolean"
|
||||
? profile.email_verified
|
||||
|
||||
Reference in New Issue
Block a user