Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 48 additions & 78 deletions src/firebase-auth-plugin.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,10 @@
import type { BetterAuthPlugin } from "better-auth";
import { APIError, createAuthEndpoint } from "better-auth/api";
import { createAuthMiddleware } from "better-auth/plugins";
import type { BetterAuthPlugin, GenericEndpointContext } from "better-auth";
import { APIError, createAuthEndpoint, createAuthMiddleware } from "better-auth/api";
import type { FirebaseOptions } from "firebase/app";
import { getAuth } from "firebase-admin/auth";
import type { AuthResponse, FirebaseAuthPluginOptions } from "./types";

// Context type for Better Auth endpoints and hooks
// Using any for adapter to maintain compatibility with Better Auth's DBAdapter type
type Context = {
context: {
adapter: any;
internalAdapter: any;
};
json: (data: any) => Promise<Response> | Response;
};
type Context = GenericEndpointContext;

type DecodedToken = {
uid: string;
Expand All @@ -30,90 +21,69 @@ const createOrUpdateUser = async (
idToken: string,
sessionExpiresInDays: number = 7,
): Promise<AuthResponse> => {
const { adapter, internalAdapter } = ctx.context;

// First, try to find user by existing Firebase account
// Check if adapter has getAccount method (some adapters may not support it)
let account = null;
if (typeof adapter.getAccount === "function") {
try {
account = await adapter.getAccount({
provider: "firebase",
providerAccountId: decodedToken.uid,
});
} catch {
// Account doesn't exist, continue
}
}
const { internalAdapter } = ctx.context;

let user = null;
if (account) {
// User exists with this Firebase account
user = await internalAdapter.getUser({ id: account.userId });
} else if (decodedToken.email) {
// Try to find user by email
user = await internalAdapter.getUser({
email: decodedToken.email,
});
const oauthUser = await internalAdapter.findOAuthUser(
decodedToken.email || "",
decodedToken.uid,
"firebase",
);

user = oauthUser?.user || null;
const account = oauthUser?.linkedAccount || null;

if (!user && decodedToken.email) {
const existingUser = await internalAdapter.findUserByEmail(decodedToken.email);
user = existingUser?.user || null;
}

if (!user) {
user = await internalAdapter.createUser({
email: decodedToken.email || null,
name: decodedToken.name || null,
image: decodedToken.picture || null,
email: decodedToken.email || "",
name: decodedToken.name || "",
image: decodedToken.picture || undefined,
emailVerified: decodedToken.email_verified || false,
});
} else {
user = await internalAdapter.updateUser({
id: user.id,
user = await internalAdapter.updateUser(user.id, {
name: decodedToken.name || user.name,
image: decodedToken.picture || user.image,
emailVerified: decodedToken.email_verified ?? user.emailVerified,
});
}

// Use upsertAccount if available, otherwise fall back to createAccount
const accountData = {
provider: "firebase",
providerAccountId: decodedToken.uid,
userId: user.id,
accessToken: idToken,
expiresAt: decodedToken.exp ? new Date(decodedToken.exp * 1000) : null,
};

if (typeof adapter.upsertAccount === "function") {
await adapter.upsertAccount(accountData);
} else if (typeof adapter.createAccount === "function") {
try {
await adapter.createAccount(accountData);
} catch (error) {
// If account already exists, ignore the error (account already exists)
// This handles cases where the account was created in a previous sign-in
if (
error instanceof Error &&
!error.message.includes("already exists") &&
!error.message.includes("unique constraint") &&
!error.message.includes("duplicate")
) {
throw error;
}
}
if (!account) {
await internalAdapter.linkAccount({
providerId: "firebase",
accountId: decodedToken.uid,
userId: user.id,
idToken: idToken,
accessTokenExpiresAt: decodedToken.exp ? new Date(decodedToken.exp * 1000) : undefined,
});
} else {
await internalAdapter.updateAccount(account.id, {
idToken: idToken,
accessTokenExpiresAt: decodedToken.exp ? new Date(decodedToken.exp * 1000) : undefined,
});
}

const session = await internalAdapter.createSession({
userId: user.id,
expiresAt: new Date(
Date.now() + 1000 * 60 * 60 * 24 * sessionExpiresInDays,
),
});
const session = await internalAdapter.createSession(
user.id,
undefined,
{
expiresAt: new Date(
Date.now() + 1000 * 60 * 60 * 24 * sessionExpiresInDays,
),
}
);

return {
user: {
id: user.id,
email: user.email,
name: user.name,
image: user.image,
image: user.image || null,
},
session: {
id: session.id,
Expand All @@ -130,7 +100,7 @@ const getFirebaseApp = async (
const apps = firebaseApp.getApps();
return apps.length === 0
? firebaseApp.initializeApp(firebaseConfig, "better-auth-firebase")
: apps[0]!;
: apps[0];
};

export const firebaseAuthPlugin = (
Expand Down Expand Up @@ -415,8 +385,10 @@ export const firebaseAuthPlugin = (
);
}

type MiddleWareCtx = Parameters<Parameters<typeof createAuthMiddleware>[0]>[0];

const handleEmailAuth = async (
ctx: Parameters<Parameters<typeof createAuthMiddleware>[0]>[0],
ctx: MiddleWareCtx,
isSignUp: boolean,
) => {
const { email, password, name } = ctx.body as {
Expand All @@ -441,9 +413,7 @@ export const firebaseAuthPlugin = (

const app = await getFirebaseApp(firebaseConfig);
const auth = getAuth(app);
let userCredential:
| Awaited<ReturnType<typeof createUserWithEmailAndPassword>>
| Awaited<ReturnType<typeof signInWithEmailAndPassword>>;
let userCredential: Awaited<ReturnType<typeof createUserWithEmailAndPassword>>;

if (isSignUp) {
userCredential = await createUserWithEmailAndPassword(
Expand Down