Securing Shopify App Authentication: Storing Access Tokens After Admin OAuth

I’m working on a Shopify app using the remix template. I want to improve how I handle the access token after the store owner approves the app. My goal is to save this token along with the store URL in AWS Secrets Manager.

Most of my app is ready. I just need to tweak some theme files automatically. That’s where the access token comes in handy.

I’ve been looking at this code:

import { authenticate } from '../shopify.server';

export const loader = async ({ request }) => {
  const authResult = await authenticate.admin(request);
  // Need help here with access token and AWS logic
  return null;
};

I’m stuck on getting the store name. Do I even need it? I thought I needed it to change the session token into an access token.

There’s also a Prisma schema that mentions the access token:

model Session {
  id String @id
  shop String
  accessToken String
  // other fields...
}

So, after auth, I should be able to get the access token, right? I’ve tried this:

import { authenticate } from '../shopify.server';
import { PrismaClient } from '@prisma/client';

const db = new PrismaClient();

export const loader = async ({ request }) => {
  const authData = await authenticate.admin(request);
  console.log('Auth data:', authData);

  const storeDomain = authData.input_data.shop.domain;
  const token = await fetchTokenForStore(storeDomain);

  console.log('Token:', token);
  return null;
};

async function fetchTokenForStore(domain) {
  const sessionData = await db.session.findUnique({
    where: { shop: domain },
  });
  return sessionData?.accessToken || null;
}

It’s not working, but feels close. Any tips on how to correctly get and save the access token?

I’ve been through this process before, and I can share some insights that might help you out.

First off, you’re on the right track with your approach. The store name (or domain) is indeed crucial for identifying the specific shop you’re working with. You don’t need it to convert the session token to an access token, but it’s essential for retrieving the correct session data.

In your code, you’re close, but there’s a small issue. The authenticate.admin() method doesn’t return the shop domain directly. Instead, it returns a session object. Try modifying your loader function like this:

export const loader = async ({ request }) => {
  const session = await authenticate.admin(request);
  const shopDomain = session.shop;
  const accessToken = session.accessToken;

  // Now you can use shopDomain and accessToken
  // to save in AWS Secrets Manager

  return null;
};

This should give you both the shop domain and the access token. From here, you can implement the AWS Secrets Manager logic to store these securely.

Remember to handle potential errors, like when the session doesn’t exist or the access token is missing. Also, consider implementing a caching mechanism to avoid hitting your database or AWS Secrets Manager too frequently.

I’ve dealt with this exact issue in my Shopify apps. Here’s what worked for me:

The authenticate.admin() method actually gives you everything you need in one go. You don’t have to query the database separately or do any extra steps. Just destructure the session object like this:

export const loader = async ({ request }) => {
  const session = await authenticate.admin(request);
  const { shop, accessToken } = session;

  // Now you've got both the shop domain and access token
  console.log('Shop:', shop);
  console.log('Access Token:', accessToken);

  // Implement your AWS Secrets Manager logic here

  return null;
};

This approach is much cleaner and more efficient. Just make sure to add proper error handling in case authentication fails.

One tip from my experience: Be cautious with how often you’re calling AWS Secrets Manager. Those API calls can add up quickly, both in terms of cost and latency. Consider implementing a caching layer, maybe using Redis or even in-memory caching, to store the tokens temporarily. This can significantly improve your app’s performance and reduce AWS costs.

hey, try getting the session directly: const session = await authenticate.admin(request); then use { shop, accessToken } = session. no extra db call needed. handle erors if they come up and maybe cache the token for faster aws calls. hope it helps!

Your approach is sound, but there’s a simpler way to handle this. The authenticate.admin() method actually returns a session object with all the info you need. You don’t have to query the database separately.

Try this modification:

export const loader = async ({ request }) => {
  const session = await authenticate.admin(request);
  const { shop, accessToken } = session;

  // Now you have both shop and accessToken
  // Implement your AWS Secrets Manager logic here

  return null;
};

This should streamline your code and eliminate the need for that extra database query. Just remember to add error handling for cases where authentication fails or the session is invalid. Also, consider implementing some form of caching to reduce calls to AWS Secrets Manager, as those can add up quickly in terms of both time and cost.

I’ve implemented similar functionality in Shopify apps before. The authenticate.admin() method actually returns a session object containing all necessary information. You can simplify your code like this:

export const loader = async ({ request }) => {
  const session = await authenticate.admin(request);
  const { shop, accessToken } = session;

  // Use shop and accessToken for AWS Secrets Manager

  return null;
};

This approach eliminates the need for additional database queries. Ensure you implement proper error handling for authentication failures. Also, consider caching the token to reduce frequent calls to AWS Secrets Manager, which can impact performance and costs. Implement a caching mechanism using Redis or in-memory storage for optimal efficiency.