@auth/firebase-adapter 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2022-2023, Balázs Orbán
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,28 @@
1
+ <p align="center">
2
+ <br/>
3
+ <a href="https://authjs.dev" target="_blank">
4
+ <img height="64px" src="https://authjs.dev/img/logo/logo-sm.png" />
5
+ </a>
6
+ <a href="https://firebase.google.com" target="_blank">
7
+ <img height="64px" src="https://authjs.dev/img/adapters/firebase.svg"/>
8
+ </a>
9
+ <h3 align="center"><b>Firebase Adapter</b> - NextAuth.js / Auth.js</a></h3>
10
+ <p align="center" style="align: center;">
11
+ <a href="https://npm.im/@auth/firebase-adapter">
12
+ <img src="https://img.shields.io/badge/TypeScript-blue?style=flat-square" alt="TypeScript" />
13
+ </a>
14
+ <a href="https://npm.im/@auth/firebase-adapter">
15
+ <img alt="npm" src="https://img.shields.io/npm/v/@auth/firebase-adapter?color=green&label=@auth/firebase-adapter&style=flat-square">
16
+ </a>
17
+ <a href="https://www.npmtrends.com/@auth/firebase-adapter">
18
+ <img src="https://img.shields.io/npm/dm/@auth/firebase-adapter?label=%20downloads&style=flat-square" alt="Downloads" />
19
+ </a>
20
+ <a href="https://github.com/nextauthjs/next-auth/stargazers">
21
+ <img src="https://img.shields.io/github/stars/nextauthjs/next-auth?style=flat-square" alt="Github Stars" />
22
+ </a>
23
+ </p>
24
+ </p>
25
+
26
+ ---
27
+
28
+ Check out the documentation at [authjs.dev](https://authjs.dev/reference/adapter/firebase).
package/index.d.ts ADDED
@@ -0,0 +1,143 @@
1
+ /**
2
+ * <div style={{display: "flex", justifyContent: "space-between", alignItems: "center", padding: 16}}>
3
+ * <span>
4
+ * Official <b>Firebase</b> adapter for Auth.js / NextAuth.js,
5
+ * using the <a href="https://firebase.google.com/docs/admin/setup">Firebase Admin SDK</a>
6
+ * &nbsp;and <a href="https://firebase.google.com/docs/firestore">Firestore</a>.</span>
7
+ * <a href="https://firebase.google.com/">
8
+ * <img style={{display: "block"}} src="https://authjs.dev/img/adapters/firebase.svg" height="48" width="48"/>
9
+ * </a>
10
+ * </div>
11
+ *
12
+ * ## Installation
13
+ *
14
+ * ```bash npm2yarn2pnpm
15
+ * npm install @auth/firebase-adapter firebase-admin
16
+ * ```
17
+ *
18
+ * @module @auth/firebase-adapter
19
+ */
20
+ import { type AppOptions } from "firebase-admin/app";
21
+ import { Firestore } from "firebase-admin/firestore";
22
+ import type { Adapter } from "@auth/core/adapters";
23
+ /** Configure the Firebase Adapter. */
24
+ export interface FirebaseAdapterConfig extends AppOptions {
25
+ /**
26
+ * The name of the app passed to {@link https://firebase.google.com/docs/reference/admin/node/firebase-admin.md#initializeapp `initializeApp()`}.
27
+ */
28
+ name?: string;
29
+ firestore?: Firestore;
30
+ /**
31
+ * Use this option if mixed `snake_case` and `camelCase` field names in the database is an issue for you.
32
+ * Passing `snake_case` will convert all field and collection names to `snake_case`.
33
+ * E.g. the collection `verificationTokens` will be `verification_tokens`,
34
+ * and fields like `emailVerified` will be `email_verified` instead.
35
+ *
36
+ *
37
+ * @example
38
+ * ```ts title="pages/api/auth/[...nextauth].ts"
39
+ * import NextAuth from "next-auth"
40
+ * import { FirestoreAdapter } from "@auth/firebase-adapter"
41
+ *
42
+ * export default NextAuth({
43
+ * adapter: FirestoreAdapter({ namingStrategy: "snake_case" })
44
+ * // ...
45
+ * })
46
+ * ```
47
+ */
48
+ namingStrategy?: "snake_case";
49
+ }
50
+ /**
51
+ * ## Setup
52
+ *
53
+ * First, create a Firebase project and generate a service account key. Visit: `https://console.firebase.google.com/u/0/project/{project-id}/settings/serviceaccounts/adminsdk` (replace `{project-id}` with your project's id)
54
+ *
55
+ * Now you have a few options to authenticate with the Firebase Admin SDK in your app:
56
+ *
57
+ * ### Environment variables
58
+ * - Download the service account key and save it in your project. (Make sure to add the file to your `.gitignore`!)
59
+ * - Add [`GOOGLE_APPLICATION_CREDENTIALS`](https://cloud.google.com/docs/authentication/application-default-credentials#GAC) to your environment variables and point it to the service account key file.
60
+ * - The adapter will automatically pick up the environment variable and use it to authenticate with the Firebase Admin SDK.
61
+ *
62
+ * @example
63
+ * ```ts title="pages/api/auth/[...nextauth].ts"
64
+ * import NextAuth from "next-auth"
65
+ * import { FirestoreAdapter } from "@auth/firebase-adapter"
66
+ *
67
+ * export default NextAuth({
68
+ * adapter: FirestoreAdapter(),
69
+ * // ...
70
+ * })
71
+ * ```
72
+ *
73
+ * ### Service account values
74
+ *
75
+ * - Download the service account key to a temporary location. (Make sure to not commit this file to your repository!)
76
+ * - Add the following environment variables to your project: `FIREBASE_PROJECT_ID`, `FIREBASE_CLIENT_EMAIL`, `FIREBASE_PRIVATE_KEY`.
77
+ * - Pass the config to the adapter, using the environment variables as shown in the example below.
78
+ *
79
+ * @example
80
+ * ```ts title="pages/api/auth/[...nextauth].ts"
81
+ * import NextAuth from "next-auth"
82
+ * import { FirestoreAdapter } from "@auth/firebase-adapter"
83
+ * import { cert } from "firebase-admin/app"
84
+ *
85
+ * export default NextAuth({
86
+ * adapter: FirestoreAdapter({
87
+ * credential: cert({
88
+ * projectId: process.env.FIREBASE_PROJECT_ID,
89
+ * clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
90
+ * privateKey: process.env.FIREBASE_PRIVATE_KEY,
91
+ * })
92
+ * })
93
+ * // ...
94
+ * })
95
+ * ```
96
+ *
97
+ * ### Using an existing Firestore instance
98
+ *
99
+ * If you already have a Firestore instance, you can pass that to the adapter directly instead.
100
+ *
101
+ * :::note
102
+ * When passing an instance and in a serverless environment, remember to handle duplicate app initialization.
103
+ * :::
104
+ *
105
+ * :::tip
106
+ * You can use the {@link initFirestore} utility to initialize the app and get an instance safely.
107
+ * :::
108
+ *
109
+ * @example
110
+ * ```ts title="pages/api/auth/[...nextauth].ts"
111
+ * import NextAuth from "next-auth"
112
+ * import { FirestoreAdapter } from "@auth/firebase-adapter"
113
+ * import { firestore } from "lib/firestore"
114
+ *
115
+ * export default NextAuth({
116
+ * adapter: FirestoreAdapter(firestore),
117
+ * // ...
118
+ * })
119
+ * ```
120
+ */
121
+ export declare function FirestoreAdapter(config?: FirebaseAdapterConfig | Firestore): Adapter;
122
+ /**
123
+ * Utility function that helps making sure that there is no duplicate app initialization issues in serverless environments.
124
+ * If no parameter is passed, it will use the `GOOGLE_APPLICATION_CREDENTIALS` environment variable to initialize a Firestore instance.
125
+ *
126
+ * @example
127
+ * ```ts title="lib/firestore.ts"
128
+ * import { initFirestore } from "@auth/firebase-adapter"
129
+ * import { cert } from "firebase-admin/app"
130
+ *
131
+ * export const firestore = initFirestore({
132
+ * credential: cert({
133
+ * projectId: process.env.FIREBASE_PROJECT_ID,
134
+ * clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
135
+ * privateKey: process.env.FIREBASE_PRIVATE_KEY,
136
+ * })
137
+ * })
138
+ * ```
139
+ */
140
+ export declare function initFirestore(options?: AppOptions & {
141
+ name?: FirebaseAdapterConfig["name"];
142
+ }): Firestore;
143
+ //# sourceMappingURL=index.d.ts.map
package/index.d.ts.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,KAAK,UAAU,EAA0B,MAAM,oBAAoB,CAAA;AAE5E,OAAO,EACL,SAAS,EAIV,MAAM,0BAA0B,CAAA;AAEjC,OAAO,KAAK,EACV,OAAO,EAKR,MAAM,qBAAqB,CAAA;AAE5B,sCAAsC;AACtC,MAAM,WAAW,qBAAsB,SAAQ,UAAU;IACvD;;OAEG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,SAAS,CAAC,EAAE,SAAS,CAAA;IACrB;;;;;;;;;;;;;;;;;OAiBG;IACH,cAAc,CAAC,EAAE,YAAY,CAAA;CAC9B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsEG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,CAAC,EAAE,qBAAqB,GAAG,SAAS,GACzC,OAAO,CAgKT;AA8HD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,aAAa,CAC3B,OAAO,GAAE,UAAU,GAAG;IAAE,IAAI,CAAC,EAAE,qBAAqB,CAAC,MAAM,CAAC,CAAA;CAAO,aAQpE"}
package/index.js ADDED
@@ -0,0 +1,327 @@
1
+ /**
2
+ * <div style={{display: "flex", justifyContent: "space-between", alignItems: "center", padding: 16}}>
3
+ * <span>
4
+ * Official <b>Firebase</b> adapter for Auth.js / NextAuth.js,
5
+ * using the <a href="https://firebase.google.com/docs/admin/setup">Firebase Admin SDK</a>
6
+ * &nbsp;and <a href="https://firebase.google.com/docs/firestore">Firestore</a>.</span>
7
+ * <a href="https://firebase.google.com/">
8
+ * <img style={{display: "block"}} src="https://authjs.dev/img/adapters/firebase.svg" height="48" width="48"/>
9
+ * </a>
10
+ * </div>
11
+ *
12
+ * ## Installation
13
+ *
14
+ * ```bash npm2yarn2pnpm
15
+ * npm install @auth/firebase-adapter firebase-admin
16
+ * ```
17
+ *
18
+ * @module @auth/firebase-adapter
19
+ */
20
+ import { getApps, initializeApp } from "firebase-admin/app";
21
+ import { Firestore, getFirestore, initializeFirestore, Timestamp, } from "firebase-admin/firestore";
22
+ /**
23
+ * ## Setup
24
+ *
25
+ * First, create a Firebase project and generate a service account key. Visit: `https://console.firebase.google.com/u/0/project/{project-id}/settings/serviceaccounts/adminsdk` (replace `{project-id}` with your project's id)
26
+ *
27
+ * Now you have a few options to authenticate with the Firebase Admin SDK in your app:
28
+ *
29
+ * ### Environment variables
30
+ * - Download the service account key and save it in your project. (Make sure to add the file to your `.gitignore`!)
31
+ * - Add [`GOOGLE_APPLICATION_CREDENTIALS`](https://cloud.google.com/docs/authentication/application-default-credentials#GAC) to your environment variables and point it to the service account key file.
32
+ * - The adapter will automatically pick up the environment variable and use it to authenticate with the Firebase Admin SDK.
33
+ *
34
+ * @example
35
+ * ```ts title="pages/api/auth/[...nextauth].ts"
36
+ * import NextAuth from "next-auth"
37
+ * import { FirestoreAdapter } from "@auth/firebase-adapter"
38
+ *
39
+ * export default NextAuth({
40
+ * adapter: FirestoreAdapter(),
41
+ * // ...
42
+ * })
43
+ * ```
44
+ *
45
+ * ### Service account values
46
+ *
47
+ * - Download the service account key to a temporary location. (Make sure to not commit this file to your repository!)
48
+ * - Add the following environment variables to your project: `FIREBASE_PROJECT_ID`, `FIREBASE_CLIENT_EMAIL`, `FIREBASE_PRIVATE_KEY`.
49
+ * - Pass the config to the adapter, using the environment variables as shown in the example below.
50
+ *
51
+ * @example
52
+ * ```ts title="pages/api/auth/[...nextauth].ts"
53
+ * import NextAuth from "next-auth"
54
+ * import { FirestoreAdapter } from "@auth/firebase-adapter"
55
+ * import { cert } from "firebase-admin/app"
56
+ *
57
+ * export default NextAuth({
58
+ * adapter: FirestoreAdapter({
59
+ * credential: cert({
60
+ * projectId: process.env.FIREBASE_PROJECT_ID,
61
+ * clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
62
+ * privateKey: process.env.FIREBASE_PRIVATE_KEY,
63
+ * })
64
+ * })
65
+ * // ...
66
+ * })
67
+ * ```
68
+ *
69
+ * ### Using an existing Firestore instance
70
+ *
71
+ * If you already have a Firestore instance, you can pass that to the adapter directly instead.
72
+ *
73
+ * :::note
74
+ * When passing an instance and in a serverless environment, remember to handle duplicate app initialization.
75
+ * :::
76
+ *
77
+ * :::tip
78
+ * You can use the {@link initFirestore} utility to initialize the app and get an instance safely.
79
+ * :::
80
+ *
81
+ * @example
82
+ * ```ts title="pages/api/auth/[...nextauth].ts"
83
+ * import NextAuth from "next-auth"
84
+ * import { FirestoreAdapter } from "@auth/firebase-adapter"
85
+ * import { firestore } from "lib/firestore"
86
+ *
87
+ * export default NextAuth({
88
+ * adapter: FirestoreAdapter(firestore),
89
+ * // ...
90
+ * })
91
+ * ```
92
+ */
93
+ export function FirestoreAdapter(config) {
94
+ const { db, namingStrategy = "default" } = config instanceof Firestore
95
+ ? { db: config }
96
+ : { ...config, db: config?.firestore ?? initFirestore(config) };
97
+ const preferSnakeCase = namingStrategy === "snake_case";
98
+ const C = collectionsFactory(db, preferSnakeCase);
99
+ const mapper = mapFieldsFactory(preferSnakeCase);
100
+ return {
101
+ async createUser(userInit) {
102
+ const { id: userId } = await C.users.add(userInit);
103
+ const user = await getDoc(C.users.doc(userId));
104
+ if (!user)
105
+ throw new Error("[createUser] Failed to fetch created user");
106
+ return user;
107
+ },
108
+ async getUser(id) {
109
+ return await getDoc(C.users.doc(id));
110
+ },
111
+ async getUserByEmail(email) {
112
+ return await getOneDoc(C.users.where("email", "==", email));
113
+ },
114
+ async getUserByAccount({ provider, providerAccountId }) {
115
+ const account = await getOneDoc(C.accounts
116
+ .where("provider", "==", provider)
117
+ .where(mapper.toDb("providerAccountId"), "==", providerAccountId));
118
+ if (!account)
119
+ return null;
120
+ return await getDoc(C.users.doc(account.userId));
121
+ },
122
+ async updateUser(partialUser) {
123
+ if (!partialUser.id)
124
+ throw new Error("[updateUser] Missing id");
125
+ const userRef = C.users.doc(partialUser.id);
126
+ await userRef.set(partialUser, { merge: true });
127
+ const user = await getDoc(userRef);
128
+ if (!user)
129
+ throw new Error("[updateUser] Failed to fetch updated user");
130
+ return user;
131
+ },
132
+ async deleteUser(userId) {
133
+ await db.runTransaction(async (transaction) => {
134
+ const accounts = await C.accounts
135
+ .where(mapper.toDb("userId"), "==", userId)
136
+ .get();
137
+ const sessions = await C.sessions
138
+ .where(mapper.toDb("userId"), "==", userId)
139
+ .get();
140
+ transaction.delete(C.users.doc(userId));
141
+ accounts.forEach((account) => transaction.delete(account.ref));
142
+ sessions.forEach((session) => transaction.delete(session.ref));
143
+ });
144
+ },
145
+ async linkAccount(accountInit) {
146
+ const ref = await C.accounts.add(accountInit);
147
+ const account = await ref.get().then((doc) => doc.data());
148
+ return account ?? null;
149
+ },
150
+ async unlinkAccount({ provider, providerAccountId }) {
151
+ await deleteDocs(C.accounts
152
+ .where("provider", "==", provider)
153
+ .where(mapper.toDb("providerAccountId"), "==", providerAccountId)
154
+ .limit(1));
155
+ },
156
+ async createSession(sessionInit) {
157
+ const ref = await C.sessions.add(sessionInit);
158
+ const session = await ref.get().then((doc) => doc.data());
159
+ if (session)
160
+ return session ?? null;
161
+ throw new Error("[createSession] Failed to fetch created session");
162
+ },
163
+ async getSessionAndUser(sessionToken) {
164
+ const session = await getOneDoc(C.sessions.where(mapper.toDb("sessionToken"), "==", sessionToken));
165
+ if (!session)
166
+ return null;
167
+ const user = await getDoc(C.users.doc(session.userId));
168
+ if (!user)
169
+ return null;
170
+ return { session, user };
171
+ },
172
+ async updateSession(partialSession) {
173
+ const sessionId = await db.runTransaction(async (transaction) => {
174
+ const sessionSnapshot = (await transaction.get(C.sessions
175
+ .where(mapper.toDb("sessionToken"), "==", partialSession.sessionToken)
176
+ .limit(1))).docs[0];
177
+ if (!sessionSnapshot?.exists)
178
+ return null;
179
+ transaction.set(sessionSnapshot.ref, partialSession, { merge: true });
180
+ return sessionSnapshot.id;
181
+ });
182
+ if (!sessionId)
183
+ return null;
184
+ const session = await getDoc(C.sessions.doc(sessionId));
185
+ if (session)
186
+ return session;
187
+ throw new Error("[updateSession] Failed to fetch updated session");
188
+ },
189
+ async deleteSession(sessionToken) {
190
+ await deleteDocs(C.sessions
191
+ .where(mapper.toDb("sessionToken"), "==", sessionToken)
192
+ .limit(1));
193
+ },
194
+ async createVerificationToken(verificationToken) {
195
+ await C.verification_tokens.add(verificationToken);
196
+ return verificationToken;
197
+ },
198
+ async useVerificationToken({ identifier, token }) {
199
+ const verificationTokenSnapshot = (await C.verification_tokens
200
+ .where("identifier", "==", identifier)
201
+ .where("token", "==", token)
202
+ .limit(1)
203
+ .get()).docs[0];
204
+ if (!verificationTokenSnapshot)
205
+ return null;
206
+ const data = verificationTokenSnapshot.data();
207
+ await verificationTokenSnapshot.ref.delete();
208
+ return data;
209
+ },
210
+ };
211
+ }
212
+ // for consistency, store all fields as snake_case in the database
213
+ const MAP_TO_FIRESTORE = {
214
+ userId: "user_id",
215
+ sessionToken: "session_token",
216
+ providerAccountId: "provider_account_id",
217
+ emailVerified: "email_verified",
218
+ };
219
+ const MAP_FROM_FIRESTORE = {};
220
+ for (const key in MAP_TO_FIRESTORE) {
221
+ MAP_FROM_FIRESTORE[MAP_TO_FIRESTORE[key]] = key;
222
+ }
223
+ const identity = (x) => x;
224
+ /** @internal */
225
+ export function mapFieldsFactory(preferSnakeCase) {
226
+ if (preferSnakeCase) {
227
+ return {
228
+ toDb: (field) => MAP_TO_FIRESTORE[field] ?? field,
229
+ fromDb: (field) => MAP_FROM_FIRESTORE[field] ?? field,
230
+ };
231
+ }
232
+ return { toDb: identity, fromDb: identity };
233
+ }
234
+ /** @internal */
235
+ function getConverter(options) {
236
+ const mapper = mapFieldsFactory(options?.preferSnakeCase ?? false);
237
+ return {
238
+ toFirestore(object) {
239
+ const document = {};
240
+ for (const key in object) {
241
+ if (key === "id")
242
+ continue;
243
+ const value = object[key];
244
+ if (value !== undefined) {
245
+ document[mapper.toDb(key)] = value;
246
+ }
247
+ else {
248
+ console.warn(`FirebaseAdapter: value for key "${key}" is undefined`);
249
+ }
250
+ }
251
+ return document;
252
+ },
253
+ fromFirestore(snapshot) {
254
+ const document = snapshot.data(); // we can guarantee it exists
255
+ const object = {};
256
+ if (!options?.excludeId) {
257
+ object.id = snapshot.id;
258
+ }
259
+ for (const key in document) {
260
+ let value = document[key];
261
+ if (value instanceof Timestamp)
262
+ value = value.toDate();
263
+ object[mapper.fromDb(key)] = value;
264
+ }
265
+ return object;
266
+ },
267
+ };
268
+ }
269
+ /** @internal */
270
+ export async function getOneDoc(querySnapshot) {
271
+ const querySnap = await querySnapshot.limit(1).get();
272
+ return querySnap.docs[0]?.data() ?? null;
273
+ }
274
+ /** @internal */
275
+ async function deleteDocs(querySnapshot) {
276
+ const querySnap = await querySnapshot.get();
277
+ for (const doc of querySnap.docs) {
278
+ await doc.ref.delete();
279
+ }
280
+ }
281
+ /** @internal */
282
+ export async function getDoc(docRef) {
283
+ const docSnap = await docRef.get();
284
+ return docSnap.data() ?? null;
285
+ }
286
+ /** @internal */
287
+ export function collectionsFactory(db, preferSnakeCase = false) {
288
+ return {
289
+ users: db
290
+ .collection("users")
291
+ .withConverter(getConverter({ preferSnakeCase })),
292
+ sessions: db
293
+ .collection("sessions")
294
+ .withConverter(getConverter({ preferSnakeCase })),
295
+ accounts: db
296
+ .collection("accounts")
297
+ .withConverter(getConverter({ preferSnakeCase })),
298
+ verification_tokens: db
299
+ .collection(preferSnakeCase ? "verification_tokens" : "verificationTokens")
300
+ .withConverter(getConverter({ preferSnakeCase, excludeId: true })),
301
+ };
302
+ }
303
+ /**
304
+ * Utility function that helps making sure that there is no duplicate app initialization issues in serverless environments.
305
+ * If no parameter is passed, it will use the `GOOGLE_APPLICATION_CREDENTIALS` environment variable to initialize a Firestore instance.
306
+ *
307
+ * @example
308
+ * ```ts title="lib/firestore.ts"
309
+ * import { initFirestore } from "@auth/firebase-adapter"
310
+ * import { cert } from "firebase-admin/app"
311
+ *
312
+ * export const firestore = initFirestore({
313
+ * credential: cert({
314
+ * projectId: process.env.FIREBASE_PROJECT_ID,
315
+ * clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
316
+ * privateKey: process.env.FIREBASE_PRIVATE_KEY,
317
+ * })
318
+ * })
319
+ * ```
320
+ */
321
+ export function initFirestore(options = {}) {
322
+ const apps = getApps();
323
+ const app = options.name ? apps.find((a) => a.name === options.name) : apps[0];
324
+ if (app)
325
+ return getFirestore(app);
326
+ return initializeFirestore(initializeApp(options, options.name));
327
+ }
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@auth/firebase-adapter",
3
+ "version": "1.0.0",
4
+ "description": "Firebase adapter for Auth.js",
5
+ "homepage": "https://authjs.dev",
6
+ "repository": "https://github.com/nextauthjs/next-auth",
7
+ "bugs": {
8
+ "url": "https://github.com/nextauthjs/next-auth/issues"
9
+ },
10
+ "author": "Ron Houben <ron.houben85@gmail.com>",
11
+ "contributors": [
12
+ "Nico Domino <yo@ndo.dev>",
13
+ "Alex Meuer <github@alexmeuer.com>"
14
+ ],
15
+ "type": "module",
16
+ "types": "./index.d.ts",
17
+ "files": [
18
+ "*.js",
19
+ "*.d.ts*",
20
+ "src"
21
+ ],
22
+ "exports": {
23
+ ".": {
24
+ "types": "./index.d.ts",
25
+ "import": "./index.js"
26
+ }
27
+ },
28
+ "license": "ISC",
29
+ "keywords": [
30
+ "next-auth",
31
+ "next.js",
32
+ "firebase",
33
+ "firebase-admin"
34
+ ],
35
+ "private": false,
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
39
+ "dependencies": {
40
+ "@auth/core": "0.8.2"
41
+ },
42
+ "peerDependencies": {
43
+ "firebase-admin": "^11.4.1"
44
+ },
45
+ "devDependencies": {
46
+ "firebase-admin": "^11.4.1",
47
+ "firebase-tools": "^11.16.1",
48
+ "jest": "^29.3.1",
49
+ "@next-auth/adapter-test": "0.0.0",
50
+ "@next-auth/tsconfig": "0.0.0"
51
+ },
52
+ "scripts": {
53
+ "dev": "tsc -w",
54
+ "build": "tsc",
55
+ "test": "firebase emulators:exec --only firestore --project next-auth-test 'jest -c tests/jest.config.js'"
56
+ }
57
+ }
package/src/index.ts ADDED
@@ -0,0 +1,452 @@
1
+ /**
2
+ * <div style={{display: "flex", justifyContent: "space-between", alignItems: "center", padding: 16}}>
3
+ * <span>
4
+ * Official <b>Firebase</b> adapter for Auth.js / NextAuth.js,
5
+ * using the <a href="https://firebase.google.com/docs/admin/setup">Firebase Admin SDK</a>
6
+ * &nbsp;and <a href="https://firebase.google.com/docs/firestore">Firestore</a>.</span>
7
+ * <a href="https://firebase.google.com/">
8
+ * <img style={{display: "block"}} src="https://authjs.dev/img/adapters/firebase.svg" height="48" width="48"/>
9
+ * </a>
10
+ * </div>
11
+ *
12
+ * ## Installation
13
+ *
14
+ * ```bash npm2yarn2pnpm
15
+ * npm install @auth/firebase-adapter firebase-admin
16
+ * ```
17
+ *
18
+ * @module @auth/firebase-adapter
19
+ */
20
+
21
+ import { type AppOptions, getApps, initializeApp } from "firebase-admin/app"
22
+
23
+ import {
24
+ Firestore,
25
+ getFirestore,
26
+ initializeFirestore,
27
+ Timestamp,
28
+ } from "firebase-admin/firestore"
29
+
30
+ import type {
31
+ Adapter,
32
+ AdapterUser,
33
+ AdapterAccount,
34
+ AdapterSession,
35
+ VerificationToken,
36
+ } from "@auth/core/adapters"
37
+
38
+ /** Configure the Firebase Adapter. */
39
+ export interface FirebaseAdapterConfig extends AppOptions {
40
+ /**
41
+ * The name of the app passed to {@link https://firebase.google.com/docs/reference/admin/node/firebase-admin.md#initializeapp `initializeApp()`}.
42
+ */
43
+ name?: string
44
+ firestore?: Firestore
45
+ /**
46
+ * Use this option if mixed `snake_case` and `camelCase` field names in the database is an issue for you.
47
+ * Passing `snake_case` will convert all field and collection names to `snake_case`.
48
+ * E.g. the collection `verificationTokens` will be `verification_tokens`,
49
+ * and fields like `emailVerified` will be `email_verified` instead.
50
+ *
51
+ *
52
+ * @example
53
+ * ```ts title="pages/api/auth/[...nextauth].ts"
54
+ * import NextAuth from "next-auth"
55
+ * import { FirestoreAdapter } from "@auth/firebase-adapter"
56
+ *
57
+ * export default NextAuth({
58
+ * adapter: FirestoreAdapter({ namingStrategy: "snake_case" })
59
+ * // ...
60
+ * })
61
+ * ```
62
+ */
63
+ namingStrategy?: "snake_case"
64
+ }
65
+
66
+ /**
67
+ * ## Setup
68
+ *
69
+ * First, create a Firebase project and generate a service account key. Visit: `https://console.firebase.google.com/u/0/project/{project-id}/settings/serviceaccounts/adminsdk` (replace `{project-id}` with your project's id)
70
+ *
71
+ * Now you have a few options to authenticate with the Firebase Admin SDK in your app:
72
+ *
73
+ * ### Environment variables
74
+ * - Download the service account key and save it in your project. (Make sure to add the file to your `.gitignore`!)
75
+ * - Add [`GOOGLE_APPLICATION_CREDENTIALS`](https://cloud.google.com/docs/authentication/application-default-credentials#GAC) to your environment variables and point it to the service account key file.
76
+ * - The adapter will automatically pick up the environment variable and use it to authenticate with the Firebase Admin SDK.
77
+ *
78
+ * @example
79
+ * ```ts title="pages/api/auth/[...nextauth].ts"
80
+ * import NextAuth from "next-auth"
81
+ * import { FirestoreAdapter } from "@auth/firebase-adapter"
82
+ *
83
+ * export default NextAuth({
84
+ * adapter: FirestoreAdapter(),
85
+ * // ...
86
+ * })
87
+ * ```
88
+ *
89
+ * ### Service account values
90
+ *
91
+ * - Download the service account key to a temporary location. (Make sure to not commit this file to your repository!)
92
+ * - Add the following environment variables to your project: `FIREBASE_PROJECT_ID`, `FIREBASE_CLIENT_EMAIL`, `FIREBASE_PRIVATE_KEY`.
93
+ * - Pass the config to the adapter, using the environment variables as shown in the example below.
94
+ *
95
+ * @example
96
+ * ```ts title="pages/api/auth/[...nextauth].ts"
97
+ * import NextAuth from "next-auth"
98
+ * import { FirestoreAdapter } from "@auth/firebase-adapter"
99
+ * import { cert } from "firebase-admin/app"
100
+ *
101
+ * export default NextAuth({
102
+ * adapter: FirestoreAdapter({
103
+ * credential: cert({
104
+ * projectId: process.env.FIREBASE_PROJECT_ID,
105
+ * clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
106
+ * privateKey: process.env.FIREBASE_PRIVATE_KEY,
107
+ * })
108
+ * })
109
+ * // ...
110
+ * })
111
+ * ```
112
+ *
113
+ * ### Using an existing Firestore instance
114
+ *
115
+ * If you already have a Firestore instance, you can pass that to the adapter directly instead.
116
+ *
117
+ * :::note
118
+ * When passing an instance and in a serverless environment, remember to handle duplicate app initialization.
119
+ * :::
120
+ *
121
+ * :::tip
122
+ * You can use the {@link initFirestore} utility to initialize the app and get an instance safely.
123
+ * :::
124
+ *
125
+ * @example
126
+ * ```ts title="pages/api/auth/[...nextauth].ts"
127
+ * import NextAuth from "next-auth"
128
+ * import { FirestoreAdapter } from "@auth/firebase-adapter"
129
+ * import { firestore } from "lib/firestore"
130
+ *
131
+ * export default NextAuth({
132
+ * adapter: FirestoreAdapter(firestore),
133
+ * // ...
134
+ * })
135
+ * ```
136
+ */
137
+ export function FirestoreAdapter(
138
+ config?: FirebaseAdapterConfig | Firestore
139
+ ): Adapter {
140
+ const { db, namingStrategy = "default" } =
141
+ config instanceof Firestore
142
+ ? { db: config }
143
+ : { ...config, db: config?.firestore ?? initFirestore(config) }
144
+
145
+ const preferSnakeCase = namingStrategy === "snake_case"
146
+ const C = collectionsFactory(db, preferSnakeCase)
147
+ const mapper = mapFieldsFactory(preferSnakeCase)
148
+
149
+ return {
150
+ async createUser(userInit) {
151
+ const { id: userId } = await C.users.add(userInit as AdapterUser)
152
+
153
+ const user = await getDoc(C.users.doc(userId))
154
+ if (!user) throw new Error("[createUser] Failed to fetch created user")
155
+
156
+ return user
157
+ },
158
+
159
+ async getUser(id) {
160
+ return await getDoc(C.users.doc(id))
161
+ },
162
+
163
+ async getUserByEmail(email) {
164
+ return await getOneDoc(C.users.where("email", "==", email))
165
+ },
166
+
167
+ async getUserByAccount({ provider, providerAccountId }) {
168
+ const account = await getOneDoc(
169
+ C.accounts
170
+ .where("provider", "==", provider)
171
+ .where(mapper.toDb("providerAccountId"), "==", providerAccountId)
172
+ )
173
+ if (!account) return null
174
+
175
+ return await getDoc(C.users.doc(account.userId))
176
+ },
177
+
178
+ async updateUser(partialUser) {
179
+ if (!partialUser.id) throw new Error("[updateUser] Missing id")
180
+
181
+ const userRef = C.users.doc(partialUser.id)
182
+
183
+ await userRef.set(partialUser, { merge: true })
184
+
185
+ const user = await getDoc(userRef)
186
+ if (!user) throw new Error("[updateUser] Failed to fetch updated user")
187
+
188
+ return user
189
+ },
190
+
191
+ async deleteUser(userId) {
192
+ await db.runTransaction(async (transaction) => {
193
+ const accounts = await C.accounts
194
+ .where(mapper.toDb("userId"), "==", userId)
195
+ .get()
196
+ const sessions = await C.sessions
197
+ .where(mapper.toDb("userId"), "==", userId)
198
+ .get()
199
+
200
+ transaction.delete(C.users.doc(userId))
201
+
202
+ accounts.forEach((account) => transaction.delete(account.ref))
203
+ sessions.forEach((session) => transaction.delete(session.ref))
204
+ })
205
+ },
206
+
207
+ async linkAccount(accountInit) {
208
+ const ref = await C.accounts.add(accountInit)
209
+ const account = await ref.get().then((doc) => doc.data())
210
+ return account ?? null
211
+ },
212
+
213
+ async unlinkAccount({ provider, providerAccountId }) {
214
+ await deleteDocs(
215
+ C.accounts
216
+ .where("provider", "==", provider)
217
+ .where(mapper.toDb("providerAccountId"), "==", providerAccountId)
218
+ .limit(1)
219
+ )
220
+ },
221
+
222
+ async createSession(sessionInit) {
223
+ const ref = await C.sessions.add(sessionInit)
224
+ const session = await ref.get().then((doc) => doc.data())
225
+
226
+ if (session) return session ?? null
227
+
228
+ throw new Error("[createSession] Failed to fetch created session")
229
+ },
230
+
231
+ async getSessionAndUser(sessionToken) {
232
+ const session = await getOneDoc(
233
+ C.sessions.where(mapper.toDb("sessionToken"), "==", sessionToken)
234
+ )
235
+ if (!session) return null
236
+
237
+ const user = await getDoc(C.users.doc(session.userId))
238
+ if (!user) return null
239
+
240
+ return { session, user }
241
+ },
242
+
243
+ async updateSession(partialSession) {
244
+ const sessionId = await db.runTransaction(async (transaction) => {
245
+ const sessionSnapshot = (
246
+ await transaction.get(
247
+ C.sessions
248
+ .where(
249
+ mapper.toDb("sessionToken"),
250
+ "==",
251
+ partialSession.sessionToken
252
+ )
253
+ .limit(1)
254
+ )
255
+ ).docs[0]
256
+ if (!sessionSnapshot?.exists) return null
257
+
258
+ transaction.set(sessionSnapshot.ref, partialSession, { merge: true })
259
+
260
+ return sessionSnapshot.id
261
+ })
262
+
263
+ if (!sessionId) return null
264
+
265
+ const session = await getDoc(C.sessions.doc(sessionId))
266
+ if (session) return session
267
+ throw new Error("[updateSession] Failed to fetch updated session")
268
+ },
269
+
270
+ async deleteSession(sessionToken) {
271
+ await deleteDocs(
272
+ C.sessions
273
+ .where(mapper.toDb("sessionToken"), "==", sessionToken)
274
+ .limit(1)
275
+ )
276
+ },
277
+
278
+ async createVerificationToken(verificationToken) {
279
+ await C.verification_tokens.add(verificationToken)
280
+ return verificationToken
281
+ },
282
+
283
+ async useVerificationToken({ identifier, token }) {
284
+ const verificationTokenSnapshot = (
285
+ await C.verification_tokens
286
+ .where("identifier", "==", identifier)
287
+ .where("token", "==", token)
288
+ .limit(1)
289
+ .get()
290
+ ).docs[0]
291
+
292
+ if (!verificationTokenSnapshot) return null
293
+
294
+ const data = verificationTokenSnapshot.data()
295
+ await verificationTokenSnapshot.ref.delete()
296
+ return data
297
+ },
298
+ }
299
+ }
300
+
301
+ // for consistency, store all fields as snake_case in the database
302
+ const MAP_TO_FIRESTORE: Record<string, string | undefined> = {
303
+ userId: "user_id",
304
+ sessionToken: "session_token",
305
+ providerAccountId: "provider_account_id",
306
+ emailVerified: "email_verified",
307
+ }
308
+ const MAP_FROM_FIRESTORE: Record<string, string | undefined> = {}
309
+
310
+ for (const key in MAP_TO_FIRESTORE) {
311
+ MAP_FROM_FIRESTORE[MAP_TO_FIRESTORE[key]!] = key
312
+ }
313
+
314
+ const identity = <T>(x: T) => x
315
+
316
+ /** @internal */
317
+ export function mapFieldsFactory(preferSnakeCase?: boolean) {
318
+ if (preferSnakeCase) {
319
+ return {
320
+ toDb: (field: string) => MAP_TO_FIRESTORE[field] ?? field,
321
+ fromDb: (field: string) => MAP_FROM_FIRESTORE[field] ?? field,
322
+ }
323
+ }
324
+ return { toDb: identity, fromDb: identity }
325
+ }
326
+
327
+ /** @internal */
328
+ function getConverter<Document extends Record<string, any>>(options: {
329
+ excludeId?: boolean
330
+ preferSnakeCase?: boolean
331
+ }): FirebaseFirestore.FirestoreDataConverter<Document> {
332
+ const mapper = mapFieldsFactory(options?.preferSnakeCase ?? false)
333
+
334
+ return {
335
+ toFirestore(object) {
336
+ const document: Record<string, unknown> = {}
337
+
338
+ for (const key in object) {
339
+ if (key === "id") continue
340
+ const value = object[key]
341
+ if (value !== undefined) {
342
+ document[mapper.toDb(key)] = value
343
+ } else {
344
+ console.warn(`FirebaseAdapter: value for key "${key}" is undefined`)
345
+ }
346
+ }
347
+
348
+ return document
349
+ },
350
+
351
+ fromFirestore(
352
+ snapshot: FirebaseFirestore.QueryDocumentSnapshot<Document>
353
+ ): Document {
354
+ const document = snapshot.data()! // we can guarantee it exists
355
+
356
+ const object: Record<string, unknown> = {}
357
+
358
+ if (!options?.excludeId) {
359
+ object.id = snapshot.id
360
+ }
361
+
362
+ for (const key in document) {
363
+ let value: any = document[key]
364
+ if (value instanceof Timestamp) value = value.toDate()
365
+
366
+ object[mapper.fromDb(key)] = value
367
+ }
368
+
369
+ return object as Document
370
+ },
371
+ }
372
+ }
373
+
374
+ /** @internal */
375
+ export async function getOneDoc<T>(
376
+ querySnapshot: FirebaseFirestore.Query<T>
377
+ ): Promise<T | null> {
378
+ const querySnap = await querySnapshot.limit(1).get()
379
+ return querySnap.docs[0]?.data() ?? null
380
+ }
381
+
382
+ /** @internal */
383
+ async function deleteDocs<T>(
384
+ querySnapshot: FirebaseFirestore.Query<T>
385
+ ): Promise<void> {
386
+ const querySnap = await querySnapshot.get()
387
+ for (const doc of querySnap.docs) {
388
+ await doc.ref.delete()
389
+ }
390
+ }
391
+
392
+ /** @internal */
393
+ export async function getDoc<T>(
394
+ docRef: FirebaseFirestore.DocumentReference<T>
395
+ ): Promise<T | null> {
396
+ const docSnap = await docRef.get()
397
+ return docSnap.data() ?? null
398
+ }
399
+
400
+ /** @internal */
401
+ export function collectionsFactory(
402
+ db: FirebaseFirestore.Firestore,
403
+ preferSnakeCase = false
404
+ ) {
405
+ return {
406
+ users: db
407
+ .collection("users")
408
+ .withConverter(getConverter<AdapterUser>({ preferSnakeCase })),
409
+ sessions: db
410
+ .collection("sessions")
411
+ .withConverter(getConverter<AdapterSession>({ preferSnakeCase })),
412
+ accounts: db
413
+ .collection("accounts")
414
+ .withConverter(getConverter<AdapterAccount>({ preferSnakeCase })),
415
+ verification_tokens: db
416
+ .collection(
417
+ preferSnakeCase ? "verification_tokens" : "verificationTokens"
418
+ )
419
+ .withConverter(
420
+ getConverter<VerificationToken>({ preferSnakeCase, excludeId: true })
421
+ ),
422
+ }
423
+ }
424
+
425
+ /**
426
+ * Utility function that helps making sure that there is no duplicate app initialization issues in serverless environments.
427
+ * If no parameter is passed, it will use the `GOOGLE_APPLICATION_CREDENTIALS` environment variable to initialize a Firestore instance.
428
+ *
429
+ * @example
430
+ * ```ts title="lib/firestore.ts"
431
+ * import { initFirestore } from "@auth/firebase-adapter"
432
+ * import { cert } from "firebase-admin/app"
433
+ *
434
+ * export const firestore = initFirestore({
435
+ * credential: cert({
436
+ * projectId: process.env.FIREBASE_PROJECT_ID,
437
+ * clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
438
+ * privateKey: process.env.FIREBASE_PRIVATE_KEY,
439
+ * })
440
+ * })
441
+ * ```
442
+ */
443
+ export function initFirestore(
444
+ options: AppOptions & { name?: FirebaseAdapterConfig["name"] } = {}
445
+ ) {
446
+ const apps = getApps()
447
+ const app = options.name ? apps.find((a) => a.name === options.name) : apps[0]
448
+
449
+ if (app) return getFirestore(app)
450
+
451
+ return initializeFirestore(initializeApp(options, options.name))
452
+ }