Dynamic Values & JWT Authentication with NextAuth.js

Hello,

I am implementing a custom authentication system using NextAuth.js because the built-in Plasmic Custom Auth module was too restrictive for my needs. I am authenticating users with Google Auth, and after a successful login, I generate and store a JWT token that should be used for future authentication.

The Issue I’m Facing

Now, I am running into a problem where I cannot access the currently authenticated user inside Plasmic Studio, which prevents me from effectively using dynamic values and building authenticated components.

In more detail:

  1. No way to access the current user in Plasmic Studio
  • I want to define a dynamic value (e.g., showing the balance of the logged-in user) in the Plasmic Data Query Builder.
  • However, there is no clear way to reference the currently authenticated user in queries.
  • Normally, I’d expect to have access to something like currentUser.email or auth.userId, but that does not seem to be possible.
  1. JWT Authentication inside Plasmic Studio
  • Since I am handling authentication through JWTs, I need Plasmic to send the JWT as part of the request when querying the database.
  • However, Plasmic’s query builder does not seem to support JWT-based authentication, meaning I cannot securely fetch user-specific data.
  1. Looping through authenticated data in repeatable elements
  • Even if I manually enter an email for testing, I also want to create repeating elements, like a transaction list that loops through a user’s balance history.
  • However, since there is no way to authenticate using a JWT within Plasmic, I cannot retrieve user-specific records dynamically.

What I Need to Solve

  • How can I pass the currently logged-in user’s email (or some kind of identifier) into Plasmic’s dynamic value queries?
  • Is there a way to make Plasmic send the JWT when making API requests, so that authentication works properly?
  • What is the recommended way to create repeatable elements that only show authenticated user-specific data when using a custom NextAuth.js implementation?

Would love to hear the best approach for integrating custom auth with dynamic values and JWT authentication inside Plasmic Studio.

Thanks!
Clemens

1 Like

I’m still working on this issue, but I feel like I’m getting closer—at least, that’s what I thought.

I’ve set up an API that queries a PostgreSQL database using a JWT for authentication. The goal is to serve the results to Plasmic Studio so I can use the query builder and see the values directly inside the Studio.

To make this work, I need Plasmic to forward the auth_token cookie with the API request. The cookie is correctly set and visible in the Plasmic context, but Plasmic does not attach it to the API call, preventing the API from authorizing the current user.

Does anyone know how to ensure that cookies are included in API calls made through Plasmic’s query builder?

Thanks in advance!

Would also be interested :eyes:

My own attempt at using NextAuth with Plasmic didn’t get very far: Need help connecting plasmic auth to next-auth/auth.js

@clemens_s hi!

I believe what you’re looking for are the code components for fetching the data. We recently updated our Supabase example, which might help you understand how the components are built. You can check it out here:

The token is populated into the Supabase SDK via cookies when you log in or when the session is refreshed. This file might interest you the most – the global context.

In this file, we pass a cookie down to the SDK so that requests include the user auth token. In the case of an editor, we use the staticToken property, which we copy from the network tab and set in the Studio under Project Settings > Context Settings. Just be sure to clear it before deploying, as it might otherwise be leaked to production.

To recap, here’s what you need to achieve your goal:

  1. Next.js server with API routes:
    You need a Next.js server with API routes that return the data you need (I assume you already have that).
  2. Next-auth API routes:
    These should already be set up.
  3. Global Context:
    Learn more about global contexts here. The global context will:
  • Populate your user data across both the app and Studio.
  • Set the token for your data-fetching components.
  • Refresh the token when needed.
  • Provide a staticToken prop, which you can modify within Studio to access user data while building components.
  1. Data Fetching Component:
    See this example.

Additionally, when working with next-auth and Plasmic, you might consider these optional steps:

  1. Middleware:
    You might want to add middleware to set up redirects for users who are not logged in. Read more here.
  2. User Data in Token and Session:
    Set up your user data in both a token and session so you can access it via useSession() hooks.
  3. Token Refresh Logic:
    Ensure that token refresh logic and expiration times are properly set up.

Finally, here’s a small example of how you can set up the GlobalContext and GlobalActionsProvider for next-auth, as discussed in step 4 as an optional path:

import React, { useEffect, useMemo, useState } from "react";
import { DataProvider, GlobalActionsProvider, usePlasmicCanvasContext } from "@plasmicapp/loader-nextjs";
import { api } from '@/pg';
import { signIn, signOut, useSession } from 'next-auth/react';
import { mutate } from 'swr';

const clearCache = () => mutate(
  () => true,
  undefined,
  { revalidate: false }
);

// These are functions your app should implement.
type User = {
  id: string;
  email: string;
  name: string;
};

// Users will be able to set these props in Studio.
interface AuthGlobalContextProps {
  // You might use this to override the auth token to a test token while developing. Make sure to clear before deploy.
  staticToken?: string;
}

const login = async (email: string, password: string) => {
  let user;
  try {
    user = await api.login(email, password);
  } catch (error) {
    return { error }
  }
  const response = await signIn('credentials', {
    accessToken: user.access_token,
    refreshToken: user.refresh_token,
    expires: user.expires,
    redirect: false,
  });

  if (!response?.error) {
    clearCache();
    await api.setToken(user.access_token);
    // you might want to do a soft redirect instead, using router.push
    // But sometimes different SDK's might struggle to apply the token.
    window.location.pathname = '/';
  } else {
    return response;
  }
};

const signup = async (userData: any) => {
  try {
    const response = await fetch('/api/auth/signup', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ userData }),
    });

    const result = await response.json();
    if (response.ok) {
      await login(userData.email, userData.password);
      return result;
    } else {
      throw new Error(result.error);
    }
  } catch (error) {
    console.error('Error during signup API call:', error);
    return { error };
  }
}

export const AuthGlobalContext = ({ children, staticToken }: React.PropsWithChildren<AuthGlobalContextProps>) => {
  const [currentUser, setCurrentUser] = useState<User | null>(null);
  const session = useSession();
  const sessionData = session?.data;

  const inStudio = !!usePlasmicCanvasContext();
  const logout = () => signOut().then(() => {
    setCurrentUser(null);
    clearCache();
    // same thing with hard refresh as in the comments for login.
    window.location.pathname = '/login';
  });

  // Get current user on mount
  useEffect(() => {
    let newToken = sessionData?.accessToken;
    if (inStudio) {
      newToken = staticToken;
    }
    if (!newToken) {
      if (sessionData?.error === 'RefreshAccessTokenError') {
        logout();
      }
      return;
    }
    api
      .setToken(newToken)
      .then(() => {
        return api.me();
      })
      .then(user => {
        if (user?.error) {
          console.log(user?.error)
          return logout();
        }
        setCurrentUser(user)
      })
      .catch(error => {
        if (error === 'Token expired.') {
          session.update();
        } else {
          console.error(error);
        }
      });
  }, [staticToken, sessionData?.accessToken, sessionData?.error]);

  const refreshTokenOnFail = (call: any) => async (...args: any) => {
    const resp = await call(...args);
    clearCache();
    if (resp?.error === 'Token expired.') {
      await session.update();
    }

    return resp;
  } 
  const actions = useMemo(() => ({
    signup,
    login,
    logout: logout,
    update: refreshTokenOnFail(api.update),
    delete: refreshTokenOnFail(api.delete),
    create: refreshTokenOnFail(api.create),
  }), []);

  return (
    <GlobalActionsProvider contextName="AuthGlobalContext" actions={actions}>
      <DataProvider name="auth" data={currentUser || {}}>
        {children}
      </DataProvider>
    </GlobalActionsProvider>
  );
}
1 Like