Skip to content

Commit 2c94c40

Browse files
committed
feat: Add support for other auth strategies
- Merge auth options of collection - Set default value for id field - Adjust the logout endpoint to handle local strategy - Bypass password check for local strategy in PayloadAdapter
1 parent 8de6ee9 commit 2c94c40

File tree

6 files changed

+184
-37
lines changed

6 files changed

+184
-37
lines changed

packages/dev/src/payload-types.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,16 @@ export interface User {
154154
| null;
155155
updatedAt: string;
156156
createdAt: string;
157+
enableAPIKey?: boolean | null;
158+
apiKey?: string | null;
159+
apiKeyIndex?: string | null;
160+
resetPasswordToken?: string | null;
161+
resetPasswordExpiration?: string | null;
162+
salt?: string | null;
163+
hash?: string | null;
164+
loginAttempts?: number | null;
165+
lockUntil?: string | null;
166+
password?: string | null;
157167
}
158168
/**
159169
* This interface was referenced by `Config`'s JSON-Schema
@@ -266,6 +276,15 @@ export interface UsersSelect<T extends boolean = true> {
266276
};
267277
updatedAt?: T;
268278
createdAt?: T;
279+
enableAPIKey?: T;
280+
apiKey?: T;
281+
apiKeyIndex?: T;
282+
resetPasswordToken?: T;
283+
resetPasswordExpiration?: T;
284+
salt?: T;
285+
hash?: T;
286+
loginAttempts?: T;
287+
lockUntil?: T;
269288
}
270289
/**
271290
* This interface was referenced by `Config`'s JSON-Schema

packages/dev/src/payload/collections/users.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ const Users: CollectionConfig = {
1111
return user?.roles?.includes("admin") ?? false;
1212
}, */
1313
},
14+
auth: {
15+
useAPIKey: true,
16+
},
17+
custom: {
18+
enableLocalStrategy: true,
19+
},
1420
fields: [
1521
{
1622
name: "name",

packages/payload-authjs/src/authjs/PayloadAdapter.ts

Lines changed: 93 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
1+
import crypto from "crypto";
12
import type {
23
Adapter,
34
AdapterAccount,
45
AdapterSession,
56
AdapterUser,
67
VerificationToken as AdapterVerificationToken,
78
} from "next-auth/adapters";
8-
import { type CollectionSlug, getPayload, type Payload, type SanitizedConfig } from "payload";
9+
import {
10+
type CollectionSlug,
11+
getPayload,
12+
type Payload,
13+
type RequiredDataFromCollectionSlug,
14+
type SanitizedConfig,
15+
} from "payload";
916
import type { Account, Session, User, VerificationToken } from "./types";
1017
import { transformObject } from "./utils/transformObject";
1118

@@ -57,12 +64,25 @@ export function PayloadAdapter({
5764
async createUser(user) {
5865
(await logger).debug({ userId: user.id, user }, `Creating user '${user.id}'`);
5966

60-
const payloadUser = (await (
61-
await payload
62-
).create({
63-
collection: userCollectionSlug,
64-
data: user,
65-
})) as User;
67+
let payloadUser: User;
68+
if (
69+
(await payload).collections[userCollectionSlug]?.config.custom.enableLocalStrategy ===
70+
true &&
71+
!(user as User).password
72+
) {
73+
// If the local strategy is enabled and the user does not have a password, bypass the password check
74+
payloadUser = (await createUserAndBypassPasswordCheck(payload, {
75+
collection: userCollectionSlug,
76+
data: user,
77+
})) as User;
78+
} else {
79+
payloadUser = (await (
80+
await payload
81+
).create({
82+
collection: userCollectionSlug,
83+
data: user,
84+
})) as User;
85+
}
6686

6787
return toAdapterUser(payloadUser);
6888
},
@@ -380,16 +400,29 @@ export function PayloadAdapter({
380400
).docs.at(0) as User | undefined;
381401

382402
if (!payloadUser) {
383-
payloadUser = (await (
384-
await payload
385-
).create({
386-
collection: userCollectionSlug,
387-
data: {
388-
id: crypto.randomUUID(),
389-
email,
390-
verificationTokens: [token],
391-
},
392-
})) as User;
403+
const user = {
404+
id: crypto.randomUUID(),
405+
email,
406+
verificationTokens: [token],
407+
};
408+
409+
if (
410+
(await payload).collections[userCollectionSlug]?.config.custom.enableLocalStrategy ===
411+
true
412+
) {
413+
// If the local strategy is enabled, bypass the password check
414+
payloadUser = (await createUserAndBypassPasswordCheck(payload, {
415+
collection: userCollectionSlug,
416+
data: user,
417+
})) as User;
418+
} else {
419+
payloadUser = (await (
420+
await payload
421+
).create({
422+
collection: userCollectionSlug,
423+
data: user,
424+
})) as User;
425+
}
393426
} else {
394427
payloadUser = (await (
395428
await payload
@@ -477,3 +510,46 @@ function toAdapterVerificationToken(
477510
...transformObject<VerificationToken, Omit<AdapterVerificationToken, "identifier">>(token),
478511
};
479512
}
513+
514+
/**
515+
* Create a user and bypass the password check
516+
* This is because payload requires a password to be set when creating a user
517+
*
518+
* @see https://github.com/payloadcms/payload/blob/main/packages/payload/src/collections/operations/create.ts#L254
519+
* @see https://github.com/payloadcms/payload/blob/main/packages/payload/src/auth/strategies/local/generatePasswordSaltHash.ts
520+
*/
521+
const createUserAndBypassPasswordCheck = async (
522+
payload: Payload | Promise<Payload>,
523+
{
524+
collection,
525+
data,
526+
}: {
527+
collection: CollectionSlug;
528+
data: RequiredDataFromCollectionSlug<CollectionSlug>;
529+
},
530+
) => {
531+
// Generate a random password
532+
data.password = crypto.randomBytes(32).toString("hex");
533+
534+
// Create the user
535+
const user = await (
536+
await payload
537+
).create({
538+
collection,
539+
data,
540+
});
541+
542+
// Remove the salt and hash after the user was created
543+
await (
544+
await payload
545+
).update({
546+
collection,
547+
id: user.id,
548+
data: {
549+
salt: null,
550+
hash: null,
551+
},
552+
});
553+
554+
return user;
555+
};

packages/payload-authjs/src/payload/AuthjsAuthStrategy.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import type { AuthjsPluginConfig } from "./plugin";
55
import { getAllVirtualFields } from "./utils/getAllVirtualFields";
66
import { getUserAttributes } from "./utils/getUserAttributes";
77

8+
export const AUTHJS_STRATEGY_NAME = "Auth.js";
9+
810
/**
911
* Auth.js Authentication Strategy for Payload CMS
1012
* @see https://payloadcms.com/docs/authentication/custom-strategies
@@ -17,7 +19,7 @@ export function AuthjsAuthStrategy(
1719
const virtualFields = getAllVirtualFields(collection.fields);
1820

1921
return {
20-
name: "Auth.js",
22+
name: AUTHJS_STRATEGY_NAME,
2123
authenticate: async ({ payload }) => {
2224
// Get session from authjs
2325
const { auth } = NextAuth(
@@ -67,7 +69,7 @@ export function AuthjsAuthStrategy(
6769
// Return user to payload cms
6870
return {
6971
user: {
70-
_strategy: "Auth.js",
72+
_strategy: AUTHJS_STRATEGY_NAME,
7173
collection: collection.slug,
7274
...payloadUser,
7375
...virtualSessionFields,

packages/payload-authjs/src/payload/collection/endpoints/logout.ts

Lines changed: 55 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import NextAuth from "next-auth";
22
import { NextResponse } from "next/server";
3-
import type { Endpoint } from "payload";
3+
import { APIError, type Endpoint, generateExpiredPayloadCookie, headersWithCors } from "payload";
44
import { withPayload } from "../../../authjs/withPayload";
5+
import { AUTHJS_STRATEGY_NAME } from "../../AuthjsAuthStrategy";
56
import type { AuthjsPluginConfig } from "../../plugin";
67
import { getRequestCollection } from "../../utils/getRequestCollection";
78

@@ -10,30 +11,68 @@ import { getRequestCollection } from "../../utils/getRequestCollection";
1011
*
1112
* @see https://payloadcms.com/docs/authentication/operations#logout
1213
* @see https://github.com/payloadcms/payload/blob/main/packages/payload/src/auth/endpoints/logout.ts
14+
* @see https://github.com/payloadcms/payload/blob/main/packages/payload/src/auth/operations/logout.ts
1315
*/
1416
export const logoutEndpoint: (pluginOptions: AuthjsPluginConfig) => Endpoint = pluginOptions => ({
1517
method: "post",
1618
path: "/logout",
1719
handler: async req => {
18-
// Sign out and get cookies from authjs
19-
const { signOut } = NextAuth(
20-
withPayload(pluginOptions.authjsConfig, {
21-
payload: req.payload,
22-
userCollectionSlug: pluginOptions.userCollectionSlug,
23-
}),
20+
const { config: collection } = getRequestCollection(req);
21+
22+
if (!req.user) {
23+
throw new APIError("No User", 400);
24+
}
25+
26+
if (req.user.collection !== collection.slug) {
27+
throw new APIError("Incorrect collection", 403);
28+
}
29+
30+
// Create response with cors headers
31+
const response = NextResponse.json(
32+
{
33+
message: req.t("authentication:logoutSuccessful"),
34+
},
35+
{
36+
headers: headersWithCors({
37+
headers: new Headers(),
38+
req,
39+
}),
40+
},
2441
);
25-
const { cookies } = await signOut({ redirect: false });
26-
27-
// Create response with cookies
28-
const response = NextResponse.json({
29-
message: req.t("authentication:logoutSuccessful"),
30-
});
31-
for (const cookie of cookies) {
32-
response.cookies.set(cookie.name, cookie.value, cookie.options);
42+
43+
if (req.user._strategy === AUTHJS_STRATEGY_NAME) {
44+
// Generate expired cookies using authjs
45+
const { signOut } = NextAuth(
46+
withPayload(pluginOptions.authjsConfig, {
47+
payload: req.payload,
48+
userCollectionSlug: pluginOptions.userCollectionSlug,
49+
}),
50+
);
51+
const { cookies } = (await signOut({ redirect: false })) as {
52+
cookies: {
53+
name: string;
54+
value: string;
55+
options: object;
56+
}[];
57+
};
58+
59+
// Set cookies on response
60+
for (const cookie of cookies) {
61+
response.cookies.set(cookie.name, cookie.value, cookie.options);
62+
}
63+
} else {
64+
// Generate an expired cookie using payload cms
65+
const expiredCookie = generateExpiredPayloadCookie({
66+
collectionAuthConfig: collection.auth,
67+
config: req.payload.config,
68+
cookiePrefix: req.payload.config.cookiePrefix,
69+
});
70+
71+
// Set cookie on response
72+
response.headers.set("Set-Cookie", expiredCookie);
3373
}
3474

3575
// Execute afterLogout hooks
36-
const { config: collection } = getRequestCollection(req);
3776
if (collection.hooks?.afterLogout?.length) {
3877
for (const hook of collection.hooks.afterLogout) {
3978
await hook({

packages/payload-authjs/src/payload/collection/index.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export const generateUsersCollection = (
3838
name: "id",
3939
type: "text",
4040
required: true,
41+
defaultValue: () => crypto.randomUUID(),
4142
access: {
4243
create: () => false,
4344
update: () => false,
@@ -87,9 +88,13 @@ export const generateUsersCollection = (
8788
};
8889

8990
// Add auth strategy to users collection
91+
const { strategies: authStrategies, ...authOptions } =
92+
typeof collection.auth === "object" ? collection.auth : {};
9093
collection.auth = {
91-
disableLocalStrategy: true,
92-
strategies: [AuthjsAuthStrategy(collection, pluginOptions)],
94+
...authOptions,
95+
strategies: [AuthjsAuthStrategy(collection, pluginOptions), ...(authStrategies ?? [])],
96+
// Disable local strategy if not explicitly enabled
97+
...(collection.custom?.enableLocalStrategy === true ? {} : { disableLocalStrategy: true }),
9398
};
9499

95100
// Add hooks to users collection

0 commit comments

Comments
 (0)