Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions src/auth/trmnlAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down
9 changes: 7 additions & 2 deletions src/utils/dbConnector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean> {
const db = getDb()
logger.debug('[ DB ] Check if we know this incoming token hash')
Expand All @@ -95,6 +97,7 @@ export async function isKnownTokenHash(tokenHash: string): Promise<boolean> {
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')
Expand All @@ -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)
Expand All @@ -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}`)
Expand All @@ -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()
Expand Down
Loading