security hardening

This commit is contained in:
Gerhard Scheikl
2026-05-31 09:35:31 +02:00
parent d7d437a871
commit 01b4734477
31 changed files with 1234 additions and 238 deletions
@@ -0,0 +1,60 @@
/**
* Thin wrapper around `@shopify/shopify-app-session-storage-prisma` that
* encrypts `accessToken` / `refreshToken` at rest using field-level AES-256-GCM.
*
* Tokens are encrypted before being handed to the underlying storage and
* decrypted after they are loaded back out. `decryptField` is backward
* compatible, so any legacy plaintext tokens already in the DB keep working.
*/
import { PrismaSessionStorage } from "@shopify/shopify-app-session-storage-prisma";
import type { Session } from "@shopify/shopify-api";
import type { PrismaClient } from "@prisma/client";
import { decryptField, encryptField } from "../crypto/fieldCrypto.server";
type SessionTokenFields = {
accessToken?: string;
refreshToken?: string;
};
function encryptTokens(session: Session): Session {
const s = session as Session & SessionTokenFields;
if (s.accessToken) s.accessToken = encryptField(s.accessToken);
if (s.refreshToken) s.refreshToken = encryptField(s.refreshToken);
return session;
}
function decryptTokens(session: Session): Session {
const s = session as Session & SessionTokenFields;
if (s.accessToken) s.accessToken = decryptField(s.accessToken);
if (s.refreshToken) s.refreshToken = decryptField(s.refreshToken);
return session;
}
/**
* Clones a Session so we never mutate the caller's instance when encrypting
* for storage. The Prisma session storage only reads plain properties, so a
* shallow structured copy via the Session class is sufficient.
*/
function cloneSession(session: Session): Session {
return Object.assign(
Object.create(Object.getPrototypeOf(session)),
session,
) as Session;
}
export class EncryptedPrismaSessionStorage extends PrismaSessionStorage<PrismaClient> {
async storeSession(session: Session): Promise<boolean> {
return super.storeSession(encryptTokens(cloneSession(session)));
}
async loadSession(id: string): Promise<Session | undefined> {
const session = await super.loadSession(id);
return session ? decryptTokens(session) : session;
}
async findSessionsByShop(shop: string): Promise<Session[]> {
const sessions = await super.findSessionsByShop(shop);
return sessions.map((s) => decryptTokens(s));
}
}