I have my front end (Next.js w/ app dir) at example.com and PayloadCMS on admin.example.com.
On my front end I'm using Clerk for user authentication. I'm trying to figure out the "best" way to architect both Payload and Next to seamlessly work together. In Payload I have a "Clients" collection containing all the users information. I set up hooks to automatically create/delete Clerk users and assign the clerk user Id to a read only field in the Client. The problem I'm trying to solve is how to make requests in Nextjs to Payload.
I see from the docs there are two ways to integeate with third-party services/apis:
- Create a user for the third-party app, and log in each time to receive a token before you attempt to access any protected actions
- Enable API key support for the Collection, where you can generate a non-expiring API key per user in the collection
Here are my two attempts at doing each.
PayloadCMS -> clerkStrategy.ts
import { Strategy as CustomStrategy } from "passport-custom";
import { sessions } from "@clerk/clerk-sdk-node";
const getCookiesAsCollection = function (
rawCookie: string
): Record<string, string> {
const cookies: Record<string, string> = {};
rawCookie &&
rawCookie.split(";").forEach(function (cookie: string) {
const parts: RegExpMatchArray | null = cookie.match(/(.*?)=(.*)$/);
if (parts && parts.length) {
cookies[parts[1].trim()] = (parts[2] || "").trim();
}
});
return cookies;
};
export class ClerkStrategy extends CustomStrategy {
name: string;
constructor() {
super(async (req, done) => {
try {
const authHeader =
req.headers.authorization?.split(" ")[1] || "";
const sessionId = authHeader as string;
const sessionToken = getCookiesAsCollection(
req.headers.cookie as string
)["__session"] as string;
const session = await sessions.verifySession(
sessionId,
sessionToken
);
console.log(session);
if (session && session.status === "active") {
done(null, { id: session.userId });
} else {
done(null, false);
}
} catch (err) {
console.log(err);
done(null, false);
}
});
this.name = "clerk";
}
}
This is calling it from client-side, but concept should be the same if I were to use route handlers or server components.
NextJS Frontend -> page.tsx
"use client";
import Image from "next/image";
import { UserButton, useSession } from "@clerk/nextjs";
export default function Home() {
const session = useSession();
const handleClick = async () => {
const sessionId = session.session?.id || "";
const res = await fetch("http://localhost:3000/api/restraunts/", {
headers: {
"Content-Type": "application/json",
Authorization: `SessionId ${sessionId}`,
},
credentials: "include",
});
const data = await res.json();
console.log(data);
};
return (
<main className="flex min-h-screen flex-col items-center justify-center p-24">
<UserButton />
<div>Hello world</div>
<button
className="bg-red-400 rounded-xl px-4 py-2 text-black hover:scale-105 transition"
onClick={handleClick}
>
Fetch Data
</button>
</main>
);
}
import qs from 'qs';
import { currentUser } from '@clerk/nextjs';
export async function getOrders () {
const user = await currentUser();
if (!user) {
return new Response('Unauthorized', { status: 401 });
}
const stringifiedQuery = qs.stringify({
where: { clerkId: { equals: user.id } },
}, { addQueryPrefix: true });
const response = await fetch(`http://localhost:3000/api/orders${stringifiedQuery}`,{
headers: {
Authorization: `admin API-Key ${process.env.PAYLOAD_API_KEY}`
}
});
// Continue to handle the response below...
}
This approach feels a lot more simple but I'm still not sure if this is the right way. Since the req.user
would be the Admin for every call, would using access controls not be possible? I'm assuming with this approach I would use queries like above and handle access in the route handler. And furthermore, would there be a way to set the Client user to req.user
?
I'm just looking for some feedback as to the pros/cons of each approach and/or if I'm on the right track. It's a little overwhelming and I don't want to overthink it, but I also don't want to be going about this the wrong way. Any feedback is helpful. Thank you!
Yes I did! My approach is probably less than ideal, but it works perfectly for what I'm doing. Ideally one would use a custom passport strategy (which I sort of had going above), but I wasn't able to fully get it. My approach has been working very well in production though:
This util will simply take the Clerk session token passed in the request auth header (or will attempt to get from cookies if no auth header) and decode/verify it using Clerk backend. (make sure CLERK_SECRET_KEY
env variable is set).
// src/util/verifyClerkUser.ts
import { sessions } from "@clerk/clerk-sdk-node";
import { decodeJwt } from "@clerk/clerk-sdk-node";
import { PayloadRequest } from "payload/types";
export default async function VerifyClerkAuthUser({
req,
}: {
req: PayloadRequest;
}) {
let token: string;
if (
req.headers.authorization === undefined ||
req.headers.authorization === null
) {
// try to get token from cookie
if (req.headers.cookie) {
const cookies = getCookies(req);
console.log(cookies);
token = cookies.__session;
} else {
return { user: null, status: 401 };
}
} else {
token = req.headers.authorization.toString().split(" ")[1];
}
try {
if (token) {
const decodeInfo = decodeJwt(token);
const sessionId = decodeInfo.payload.sid;
const verifiedSession = await sessions.verifySession(
sessionId,
token
);
if (
verifiedSession &&
verifiedSession.userId &&
verifiedSession.status === "active"
) {
return {
user: verifiedSession.userId,
status: 200,
};
} else {
return {
user: null,
status: 401,
};
}
} else {
return {
user: null,
status: 401,
};
}
} catch (err) {
console.log(err);
return {
user: null,
status: 500,
};
}
}
function getCookies(req: PayloadRequest) {
const cookies: { [key: string]: string } = {};
req.headers &&
req.headers.cookie &&
req.headers.cookie.split(";").forEach((cookie) => {
const parts = cookie.match(/(.*?)=(.*)$/);
if (parts) {
cookies[parts[1].trim()] = (parts[2] || "").trim();
}
});
return cookies;
}
Then in my routes I simply pass the req
to our function
// GET /api/user
import { PayloadHandler } from "payload/config";
import { Subscription } from "payload/generated-types";
import VerifyClerkAuthUser from "../util/verifyClerkUser";
const getUserInfo: PayloadHandler = async (req, res): Promise<void> => {
const { user, payload } = req;
const clerkAuthUser = await VerifyClerkAuthUser({ req });
if (!clerkAuthUser || !clerkAuthUser.user || clerkAuthUser.status !== 200) {
res.status(401).json({ message: "Unauthorized" });
return;
}
// you might want to move below into your util function above, but I didn't because we might not always want to get the user
try {
const {
docs: [payloadClient],
} = await payload.find({
collection: "clients",
where: {
clerkID: {
equals: (await clerkAuthUser).user,
},
},
});
// rest of code...
And finally in your frontend or wherever (below is in a client component in Next.Js)
import { useAuth } from "@clerk/nextjs";
// ...
const { sessionId, getToken } = useAuth();
const response = await fetch(
process.env.NEXT_PUBLIC_CMS_URL + "/api/user",
{
method: "GET",
headers: {
Authorization: `Bearer ${await getToken()}`,
},
}
);
const data = await response.json();
Clerk has a way to get the session token from backend too using import { auth } from "@clerk/nextjs";
so that you can hit the same endpoint securely.
Payload CMS with Clerk seems like a great idea. Did you end up answering your own questions and getting Clerk to work nicely with Payload?
Yes I did! My approach is probably less than ideal, but it works perfectly for what I'm doing. Ideally one would use a custom passport strategy (which I sort of had going above), but I wasn't able to fully get it. My approach has been working very well in production though:
This util will simply take the Clerk session token passed in the request auth header (or will attempt to get from cookies if no auth header) and decode/verify it using Clerk backend. (make sure CLERK_SECRET_KEY
env variable is set).
// src/util/verifyClerkUser.ts
import { sessions } from "@clerk/clerk-sdk-node";
import { decodeJwt } from "@clerk/clerk-sdk-node";
import { PayloadRequest } from "payload/types";
export default async function VerifyClerkAuthUser({
req,
}: {
req: PayloadRequest;
}) {
let token: string;
if (
req.headers.authorization === undefined ||
req.headers.authorization === null
) {
// try to get token from cookie
if (req.headers.cookie) {
const cookies = getCookies(req);
console.log(cookies);
token = cookies.__session;
} else {
return { user: null, status: 401 };
}
} else {
token = req.headers.authorization.toString().split(" ")[1];
}
try {
if (token) {
const decodeInfo = decodeJwt(token);
const sessionId = decodeInfo.payload.sid;
const verifiedSession = await sessions.verifySession(
sessionId,
token
);
if (
verifiedSession &&
verifiedSession.userId &&
verifiedSession.status === "active"
) {
return {
user: verifiedSession.userId,
status: 200,
};
} else {
return {
user: null,
status: 401,
};
}
} else {
return {
user: null,
status: 401,
};
}
} catch (err) {
console.log(err);
return {
user: null,
status: 500,
};
}
}
function getCookies(req: PayloadRequest) {
const cookies: { [key: string]: string } = {};
req.headers &&
req.headers.cookie &&
req.headers.cookie.split(";").forEach((cookie) => {
const parts = cookie.match(/(.*?)=(.*)$/);
if (parts) {
cookies[parts[1].trim()] = (parts[2] || "").trim();
}
});
return cookies;
}
Then in my routes I simply pass the req
to our function
// GET /api/user
import { PayloadHandler } from "payload/config";
import { Subscription } from "payload/generated-types";
import VerifyClerkAuthUser from "../util/verifyClerkUser";
const getUserInfo: PayloadHandler = async (req, res): Promise<void> => {
const { user, payload } = req;
const clerkAuthUser = await VerifyClerkAuthUser({ req });
if (!clerkAuthUser || !clerkAuthUser.user || clerkAuthUser.status !== 200) {
res.status(401).json({ message: "Unauthorized" });
return;
}
// you might want to move below into your util function above, but I didn't because we might not always want to get the user
try {
const {
docs: [payloadClient],
} = await payload.find({
collection: "clients",
where: {
clerkID: {
equals: (await clerkAuthUser).user,
},
},
});
// rest of code...
And finally in your frontend or wherever (below is in a client component in Next.Js)
import { useAuth } from "@clerk/nextjs";
// ...
const { sessionId, getToken } = useAuth();
const response = await fetch(
process.env.NEXT_PUBLIC_CMS_URL + "/api/user",
{
method: "GET",
headers: {
Authorization: `Bearer ${await getToken()}`,
},
}
);
const data = await response.json();
Clerk has a way to get the session token from backend too using import { auth } from "@clerk/nextjs";
so that you can hit the same endpoint securely.
Star
Discord
online
Get help straight from the Payload team with an Enterprise License.