From 50963645cd72e2609fff66f450a1028c5ecb33ae Mon Sep 17 00:00:00 2001 From: asubowo Date: Sat, 11 Jul 2026 00:37:56 -0400 Subject: [PATCH] Allow TRMNL install_success to rebind uuid on an existing token A valid, recognized access token is already the proof of identity for a connection, so a fresh install_success call should be trusted to update the bound uuid instead of being permanently locked to whatever was first recorded. Also log both the bound and presented uuid on a mismatch so a stuck row can be diagnosed and fixed without guessing. Co-Authored-By: Claude Sonnet 5 --- src/auth/trmnlAuth.ts | 8 ++++++-- src/utils/dbConnector.ts | 9 +++++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/auth/trmnlAuth.ts b/src/auth/trmnlAuth.ts index 68fd79a..d1c2ff6 100644 --- a/src/auth/trmnlAuth.ts +++ b/src/auth/trmnlAuth.ts @@ -36,10 +36,12 @@ export const requireTrmnlAuth: RequestHandler = async (req: Request, res: Respon } // Strip 'Bearer ' and only hash token + // Bearer token is unique to user + plugin - the users `access_token` + // we must persist this token as it we'll use it to authenticate the screen generation requests from TRMNL const tokenHash = sha256(auth.slice(7).trim()) if (!(await isKnownTokenHash(tokenHash))) { - logger.info(`[AUTH] Unrecognized TRMNL token hash - ${req.originalUrl}`) + logger.warn(`[AUTH] Unrecognized TRMNL token hash - ${req.originalUrl}`) logger.debug(tokenHash) res.status(401).json({ error: 'Unauthorized', message: 'Access Denied'}) return @@ -88,7 +90,9 @@ export const requireTrmnlUuidMatch: RequestHandler = async (req: Request, res: R } if (bound !== uuid) { - logger.warn(`[AUTH] token/uuid mismatch for ${tokenHash} - ${req.originalUrl}`) + logger.warn( + `[AUTH] token/uuid mismatch for ${tokenHash} - bound=${bound} presented=${uuid} - ${req.originalUrl}` + ) res.status(401).json({ error: 'Unauthorized', message: 'Unauthorized access' }) return } diff --git a/src/utils/dbConnector.ts b/src/utils/dbConnector.ts index 144ab5e..e2d7cd4 100644 --- a/src/utils/dbConnector.ts +++ b/src/utils/dbConnector.ts @@ -79,6 +79,8 @@ export async function storeTrmnlToken(tokenHash: string) { ).run(tokenHash, Date.now()) } +// access tokens are sha256 hashed before storage in DB for security +// this checks if we've seen this hash before export async function isKnownTokenHash(tokenHash: string): Promise { const db = getDb() logger.debug('[ DB ] Check if we know this incoming token hash') @@ -95,6 +97,7 @@ export async function isKnownTokenHash(tokenHash: string): Promise { return !!row && row.revoked_at == null } +// Update the last_seen_at timestamp for a given token hash export async function touchTrmnlToken(tokenHash: string) { const db = getDb() logger.info('[ DB ] Refreshing user token') @@ -107,6 +110,7 @@ export async function touchTrmnlToken(tokenHash: string) { ).run(Date.now(), tokenHash) } +// based on the access token (unique to user + plugin, hashed) get the user uuid that's bound to it, if any export function getUserUuidByTokenHash(tokenHash: string): string | null { const db = getDb() logger.info('[ DB ] Retrieving UUID for hash: ', tokenHash) @@ -123,7 +127,8 @@ export function getUserUuidByTokenHash(tokenHash: string): string | null { return row?.user_uuid ?? null } -// bind once: only set if currently null/empty +// Holding a valid, recognized access token is the actual proof of identity here (see requireTrmnlAuth), +// so a fresh install_success call is trusted to (re)bind the uuid rather than being locked to whatever was first recorded. export function bindUserUuidToToken(tokenHash: string, userUuid: string) { const db = getDb() logger.info(`[ DB ] Binding user to ${tokenHash}`) @@ -132,11 +137,11 @@ export function bindUserUuidToToken(tokenHash: string, userUuid: string) { update trmnl_connections set user_uuid = ? where access_token_hash = ? - and (user_uuid is null or user_uuid = '') ` ).run(userUuid, tokenHash) } +// remove outdated hashed access tokens from the database (revoked or stale) export function pruneStaleTokens() { const db = getDb() const now = Date.now()