security hardening
This commit is contained in:
@@ -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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user