payloadcms / payload

Payload is the open-source, fullstack Next.js framework, giving you instant backend superpowers. Get a full TypeScript backend and admin panel instantly. Use Payload as a headless CMS or for building powerful applications.
https://payloadcms.com
MIT License
22.27k stars 1.35k forks source link

Prevent authenticated user from navigating to admin cms in nextjs14 monorepo #4932

Closed Lorentzo92 closed 6 months ago

Lorentzo92 commented 6 months ago

Link to reproduction

No response

Describe the Bug

Hi,

We have a monorepo with nextjs14 app router and payload. In payload we have admins and users collections, only admin can access the admin panel. The routes structure is:

The issues we have is: if i login in nextjs /login as users i am correctly redirected to /, now if i try to go to /cms then payload remains pending since i am a user and i cannot access the admin panel, but i am not redirected, i only get an consolo log error "you are not allowed to perform this action"

I read through the doc but i could find a way to redirect this scenario.

To Reproduce

i can provide code if needed

Payload Version

2.8.2

Adapters and Plugins

No response

DanRibbens commented 6 months ago

To summarize your issue, when a user fails the access.admin check when visiting the admin UI, they are not warned that they are unauthorized and you can't redirect that user.

If we just provide a config property for redirecting them somewhere else that feels incomplete to me. Payload always provides meaningful defaults whenever possible so without setting that property and making a page for it you would have bad ux and it is bad dx to have to do that.

Would it be expected to be redirected to /admin/unauthorized which would show an unauthorized error?

From there we could use a new config property used to set the URL for a link to leave or log out in case they want to stay at the admin but log in with another account.

How does this sound? This looks more like a new feature to me.

Lorentzo92 commented 6 months ago

Hi Dan,

Thanks for the reply. I agree with you, it is more like a feature and the /admin/unauthorized makes sense, of even just redirect to /login since the user is not authorized he/she should not be able to login, even if the payload-token exists in the cookies.

In the meantime i found a workaround by adding a middleware before mounting payload:

app.use("/cms", async (req, res, next) => {
  const payload_user = await validateAccess(req);
  if (payload_user) {
    if (payload_user.user_type === "admins") {
      next();
    } else {
      res.redirect("../");
    }
  } else {
    next();
  }
});

where validateAccess is

import { Request } from "express";
import { PayloadJWT } from "../../types";

const SECRET_COOKIE_NAME = "payload-token";

export const validateAccess = async (request: Request) => {
  // validate
  const cookieStore = request.cookies;
  let token = "";
  if (SECRET_COOKIE_NAME in cookieStore) {
    const tokenCookie = cookieStore[SECRET_COOKIE_NAME];

    if (typeof tokenCookie !== "undefined") {
      token = tokenCookie;
    }
  }
  if (token.length === 0) {
    return undefined;
  }
  try {
    // Convert the string to a Uint8Array
    const encoder = new TextEncoder();
    const secretData = encoder.encode(process.env.PAYLOAD_SECRET!);

    // Create a hash of the secret
    const hashBuffer = await crypto.subtle.digest("SHA-256", secretData);

    // Convert the hash to a hexadecimal string
    const hashArray = Array.from(new Uint8Array(hashBuffer));
    const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");

    // Slice the first 32 characters
    const secret = hashHex.slice(0, 32);

    const [headerB64, payloadB64, signatureB64] = token.split(".");
    const header = JSON.parse(atob(headerB64));
    const payload = JSON.parse(atob(payloadB64)) as PayloadJWT;
    return payload;

    // // console.log([headerB64, payloadB64].join("."));
    // console.log("payload:", payload);

    // const data = new TextEncoder().encode([headerB64, payloadB64].join("."));
    // const signature = Uint8Array.from(atob(signatureB64.trim()), (c) => c.charCodeAt(0));
  } catch (err) {
    console.error(err);
    return undefined;
  }
};
jmikrut commented 6 months ago

Hey @Lorentzo92 thanks for following up with your solution here. Gonna convert to discussion so others can find it!