61 lines
2.2 KiB
TypeScript
61 lines
2.2 KiB
TypeScript
/**
|
|
* 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));
|
|
}
|
|
}
|