payload-oauth2
Version:
OAuth2 plugin for Payload CMS
192 lines • 8.66 kB
JavaScript
import { SignJWT } from "jose";
import crypto from "node:crypto";
import { generatePayloadCookie, getFieldsToSign } from "payload";
import { defaultCallbackExtractToken } from "./default-callback-extract-token";
import { defaultGetToken } from "./default-get-token";
export const createCallbackEndpoint = (pluginOptions) => {
const handler = async (req) => {
try {
// /////////////////////////////////////
// shorthands
// /////////////////////////////////////
const subFieldName = pluginOptions.subField?.name || pluginOptions.subFieldName || "sub";
const authCollection = (pluginOptions.authCollection ||
"users");
const collectionConfig = req.payload.collections[authCollection].config;
const payloadConfig = req.payload.config;
const callbackPath = pluginOptions.callbackPath || "/oauth/callback";
const redirectUri = `${pluginOptions.serverURL}/api/${authCollection}${callbackPath}`;
const useEmailAsIdentity = pluginOptions.useEmailAsIdentity ?? false;
const excludeEmailFromJwtToken = !useEmailAsIdentity || pluginOptions.excludeEmailFromJwtToken || false;
const onUserNotFoundBehavior = pluginOptions.onUserNotFoundBehavior || "create";
const callbackExtractToken = pluginOptions.callbackExtractToken || defaultCallbackExtractToken;
// /////////////////////////////////////
// extract code from request
// /////////////////////////////////////
const code = await callbackExtractToken(req);
// /////////////////////////////////////
// beforeOperation - Collection
// /////////////////////////////////////
// Not implemented - reserved for future use
// /////////////////////////////////////
// obtain access token or id_token
// /////////////////////////////////////
let token;
if (pluginOptions.getToken) {
token = await pluginOptions.getToken(code, req);
}
else {
token = await defaultGetToken(pluginOptions.tokenEndpoint, pluginOptions.clientId, pluginOptions.clientSecret, redirectUri, code);
}
if (typeof token !== "string") {
throw new Error(`Invalid token response: ${token}`);
}
// /////////////////////////////////////
// get user info
// /////////////////////////////////////
const userInfo = await pluginOptions.getUserInfo(token, req);
// /////////////////////////////////////
// ensure user exists
// /////////////////////////////////////
let existingUser;
if (useEmailAsIdentity) {
// Use email as the unique identifier
existingUser = await req.payload.find({
req,
collection: authCollection,
where: { email: { equals: userInfo.email } },
showHiddenFields: true,
limit: 1,
});
}
else {
// Use provider's sub field as the unique identifier
existingUser = await req.payload.find({
req,
collection: authCollection,
where: { [subFieldName]: { equals: userInfo[subFieldName] } },
showHiddenFields: true,
limit: 1,
});
}
let user = existingUser.docs[0];
if (!user) {
if (onUserNotFoundBehavior === "error") {
throw new Error(`User not found: ${useEmailAsIdentity ? userInfo.email : userInfo[subFieldName]}`);
}
else if (onUserNotFoundBehavior === "create") {
// Create new user if they don't exist
// Generate secure random password for OAuth users
userInfo.password = crypto.randomBytes(32).toString("hex");
userInfo.collection = authCollection;
const result = await req.payload.create({
req,
collection: authCollection,
data: userInfo,
showHiddenFields: true,
});
user = result;
}
else {
throw new Error(`Invalid onUserNotFoundBehavior: ${onUserNotFoundBehavior}`);
}
}
else {
// Update existing user with latest info from provider
userInfo.collection = authCollection;
const result = await req.payload.update({
req,
collection: authCollection,
id: user.id,
data: userInfo,
showHiddenFields: true,
});
user = result;
}
// /////////////////////////////////////
// beforeLogin - Collection
// /////////////////////////////////////
await collectionConfig.hooks.beforeLogin.reduce(async (priorHook, hook) => {
await priorHook;
const hookResult = await hook({
collection: collectionConfig,
context: req.context || {},
req,
user,
});
if (hookResult) {
user = hookResult;
}
}, Promise.resolve());
// /////////////////////////////////////
// login - OAuth2
// /////////////////////////////////////
const fieldsToSign = getFieldsToSign({
collectionConfig,
email: excludeEmailFromJwtToken ? "" : user.email || "",
user: user,
});
const jwtToken = await new SignJWT(fieldsToSign)
.setProtectedHeader({ alg: "HS256" })
.setExpirationTime(`${collectionConfig.auth.tokenExpiration} secs`)
.sign(new TextEncoder().encode(req.payload.secret));
req.user = user;
// /////////////////////////////////////
// afterLogin - Collection
// /////////////////////////////////////
await collectionConfig.hooks.afterLogin.reduce(async (priorHook, hook) => {
await priorHook;
const hookResult = await hook({
collection: collectionConfig,
context: req.context || {},
req,
token: jwtToken,
user,
});
if (hookResult) {
user = hookResult;
}
}, Promise.resolve());
// /////////////////////////////////////
// afterRead - Fields
// /////////////////////////////////////
// Not implemented - reserved for future use
// /////////////////////////////////////
// generate and set cookie
// /////////////////////////////////////
const cookie = generatePayloadCookie({
collectionAuthConfig: collectionConfig.auth,
cookiePrefix: payloadConfig.cookiePrefix,
token: jwtToken,
});
// /////////////////////////////////////
// success redirect
// /////////////////////////////////////
return new Response(null, {
headers: {
"Set-Cookie": cookie,
Location: await pluginOptions.successRedirect(req, jwtToken),
},
status: 302,
});
}
catch (error) {
// /////////////////////////////////////
// failure redirect
// /////////////////////////////////////
return new Response(null, {
headers: {
"Content-Type": "application/json",
Location: await pluginOptions.failureRedirect(req, error),
},
status: 302,
});
}
};
const path = pluginOptions.callbackPath || "/oauth/callback";
return [
{ method: "get", path, handler },
{ method: "post", path, handler },
];
};
//# sourceMappingURL=callback-endpoint.js.map