From 45d05d7ab8b8dabb6be157574cc5af5df3c78028 Mon Sep 17 00:00:00 2001 From: Demian Date: Sat, 11 Apr 2026 01:12:40 -0400 Subject: [PATCH] fix: hash refresh tokens at rest in the database MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Store SHA-256(token) in the database instead of the raw token value. The raw token is returned to the caller only; the hash is used for all DB lookups and updates. Existing plaintext tokens become invalid on deploy — users must re-login (acceptable per spec). - Add private hashToken() helper using crypto.sha256 - generateRefreshToken: saves hash, returns raw value - refreshAccessToken: hashes incoming token before lookup - revokeRefreshToken: hashes incoming token before update Closes #96 --- backend/src/modules/auth/auth.service.ts | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/backend/src/modules/auth/auth.service.ts b/backend/src/modules/auth/auth.service.ts index 646febb..61e67c9 100644 --- a/backend/src/modules/auth/auth.service.ts +++ b/backend/src/modules/auth/auth.service.ts @@ -79,31 +79,31 @@ export class AuthService { return result; } + private hashToken(raw: string): string { + return crypto.createHash('sha256').update(raw).digest('hex'); + } + async generateRefreshToken(userId: number): Promise { - // Generate a random token - const token = crypto.randomBytes(32).toString('hex'); + const raw = crypto.randomBytes(32).toString('hex'); - // Calculate expiry (7 days from now) const expiresAt = new Date(); expiresAt.setDate(expiresAt.getDate() + 7); - // Save to database await this.refreshTokenRepository.save({ - token, + token: this.hashToken(raw), userId, expiresAt, revoked: false, }); - return token; + return raw; } async refreshAccessToken( refreshToken: string, ): Promise<{ access_token: string; refresh_token: string }> { - // Find the refresh token const storedToken = await this.refreshTokenRepository.findOne({ - where: { token: refreshToken }, + where: { token: this.hashToken(refreshToken) }, relations: ['user'], }); @@ -111,7 +111,6 @@ export class AuthService { throw new UnauthorizedException('Invalid refresh token'); } - // Check if token is expired or revoked if (storedToken.revoked || new Date() > storedToken.expiresAt) { throw new UnauthorizedException('Refresh token expired or revoked'); } @@ -121,7 +120,6 @@ export class AuthService { revoked: true, }); - // Generate new tokens const payload = { username: storedToken.user.username, sub: storedToken.user.id, @@ -138,7 +136,10 @@ export class AuthService { } async revokeRefreshToken(token: string): Promise { - await this.refreshTokenRepository.update({ token }, { revoked: true }); + await this.refreshTokenRepository.update( + { token: this.hashToken(token) }, + { revoked: true }, + ); } async requestPasswordReset(email: string): Promise<{ message: string }> {