-
My organization requires the use of Okta, so I have to set it up with Payload. I've read the Payload docs about authentication strategies and saw that it uses Passport. There's a passport-okta-oauth package, but I experience some issues. ContextThe first thing I did was to create the strategy:
const OktaStrategy = require("passport-okta-oauth").Strategy;
export default (ctx) => {
return new OktaStrategy(
{
audience: process.env["OKTA_BASE_URL"],
clientID: process.env["OKTA_CLIENT_ID"],
clientSecret: process.env["OKTA_CLIENT_SECRET"],
scope: ["openid", "email", "profile"],
response_type: "code",
callbackURL: process.env["OKTA_CALLBACK_URL"],
},
function (accessToken, refreshToken, profile, done) {
console.log(`logged in as ${profile.displayName}`);
return done(null, {
collection: "users",
email: "[email protected]",
id: "62fb90287bed9b334e530765",
});
}
);
}; …and add it to my Users collection:
import OktaStrategy from "./auth/OktaStrategy";
const Users: CollectionConfig = {
auth: {
disableLocalStrategy: true,
strategies: [
{
name: "okta",
strategy: OktaStrategy,
},
],
},
}; Then, I started getting errors on build:
I found out in the Payload docs for Webpack:
So I aliased the module to a new, empty one: const oktaStrategyPath = path.resolve(__dirname, "collections/auth/OktaStrategy.ts");
const mockModulePath = path.resolve(__dirname, "mocks/emptyObject.ts");
export default buildConfig({
admin: {
webpack: (config) => {
return {
...config,
resolve: {
...config.resolve,
alias: {
...config.resolve.alias,
[oktaStrategyPath]: mockModulePath,
},
},
};
},
},
}); Now I was able to start Payload, but got the following error:
Looking at the Payload source, I could see that it doesn't really like sessions, because it has Anyway, I installed import express from "express";
import session from "express-session";
import passport from "passport";
import payload from "payload";
require("dotenv").config();
const app = express();
app.use(
session({
secret: "some-secret",
resave: true,
saveUninitialized: true,
})
);
app.get("/", (_, res) => {
res.redirect("/admin");
});
payload.init({
secret: process.env.PAYLOAD_SECRET,
mongoURL: process.env.MONGODB_URI,
express: app,
onInit: () => {
payload.logger.info(`Payload Admin URL: ${payload.getAdminURL()}`);
},
});
app.get(
"/authorization-code/callback",
passport.authenticate("users-okta"),
function (req, res, next) {
req.user = {
collection: "users",
email: "[email protected]",
id: "62fb90287bed9b334e530765",
};
res.redirect("/admin");
}
);
app.listen(process.env.PORT || 3000); Notice: I had to use At this point, I started getting a blank Payload admin and a request to Okta correctly responds with the sign-in page, but that's an AJAX call and instead of redirecting, Payload expects JSON, which leads to this error:
Luckily, the response is still valid and comes back with status code 200: Just for the sake of testing, I double-click on the request to open it in the browser and check if the rest follows through. It doesn't, of course, and I get this error:
After checking the Passport docs about sessions, I add this below passport.serializeUser(function (user, done) {
done(null, user);
});
passport.deserializeUser(function (user, done) {
done(null, user);
}); After retrying, I could finally see that the authentication goes through, because the
I was redirected back to I poked around the source code and found out that Payload uses a JWT to authenticate, so I decided to modify my callback handler to create the same token: import getCookieExpiration from "payload/dist/utilities/getCookieExpiration";
import jwt from "jsonwebtoken";
// ...
app.get(
"/authorization-code/callback",
passport.authenticate("users-okta"),
function (req, res, next) {
const collectionConfig = payload.collections.users.config;
const token = jwt.sign(
{
collection: "users",
email: "[email protected]",
id: "62fb90287bed9b334e530765",
},
payload.secret,
{
expiresIn: collectionConfig.auth.tokenExpiration,
}
);
const cookieOptions: CookieOptions = {
path: "/",
httpOnly: true,
expires: getCookieExpiration(collectionConfig.auth.tokenExpiration),
secure: collectionConfig.auth.cookies.secure,
sameSite: collectionConfig.auth.cookies.sameSite,
domain: undefined,
};
if (collectionConfig.auth.cookies.domain)
cookieOptions.domain = collectionConfig.auth.cookies.domain;
res.cookie(`${payload.config.cookiePrefix}-token`, token, cookieOptions);
req.user = {
collection: "users",
email: "[email protected]",
id: "62fb90287bed9b334e530765",
};
res.redirect("/admin");
}
); …but nothing changed. I was still getting a blank screen. To verify that the generated token is in fact correct, I temporarily re-enabled the default auth strategy, opened an incognito window, and manually added the cookie with the token: document.cookie = "payload-token=eyJhbGciOiJIUz…RFic"; When I navigated to Payload, I was logged in, therefore my JWT was correct. QuestionsIt appears that there's some incompatibility between Payload and this particular Passport.js setup. How am I supposed to implement authentication in this case? I think there are two approaches:
Does anyone have ideas? |
Beta Was this translation helpful? Give feedback.
Replies: 2 comments
-
Hey @hdodov - first up, I just want to note that I know exactly what you're facing here, and the truth is that implementing SSO with any application that offers an admin UI as well as API endpoints presents quite a challenge for many reasons. The Passport strategy that you've found, indeed, is not 100% applicable to Payload because we are more than a "SaaS" app. As in, all of our API endpoints also need to be able to authenticate a user, and you can't just simply redirect a non-authenticated user over to an Okta screen to authenticate. You might be able to do that for any admin panel routes, but for all API routes, they should not redirect and just return normally as if the user is not logged in. As you expect, you need to rather consider some type of "bearer strategy" where you can provide a token via cookie or a header that will allow the user to identify themselves. And then you'd need to, as you mention, add your own Express routes for redirection from your identity provider, setting HTTP-only cookie tokens, logging out, refreshing, etc. Long story short, this is challenging stuff—especially if you want to ensure that provided tokens are verified using the encryption protocol that the identity provider offers. I'm not sure if you've seen, but all of this complexity is why we offer an official Payload SSO plugin for our enterprise customers. Our SSO plugin handles all of this seamlessly and is tested with Okta out of the box. We built our own Passport strategy and wrote token verification, automatic silent refreshing, etc. all into it but it was no small task. You can indeed build this via a plugin yourself if you're up for the challenge but there is certainly a good amount of work involved. If you're up for learning more about our enterprise offering, including our SSO plugin, I'd be happy to set up a meeting sometime! |
Beta Was this translation helpful? Give feedback.
-
hey @hdodov, I have to admit I faced similar issues when implementing a strategy for
where maybe you can find some good tips/info |
Beta Was this translation helpful? Give feedback.
Hey @hdodov - first up, I just want to note that I know exactly what you're facing here, and the truth is that implementing SSO with any application that offers an admin UI as well as API endpoints presents quite a challenge for many reasons.
The Passport strategy that you've found, indeed, is not 100% applicable to Payload because we are more than a "SaaS" app. As in, all of our API endpoints also need to be able to authenticate a user, and you can't just simply redirect a non-authenticated user over to an Okta screen to authenticate. You might be able to do that for any admin panel routes, but for all API routes, they should not redirect and just return normally as if the user is not lo…