From a8415474e1c8bb93cf931905dee8bcaed046dba3 Mon Sep 17 00:00:00 2001 From: Sourav Kashyap Date: Fri, 19 Jun 2026 18:16:07 +0530 Subject: [PATCH] fix(core): revoke active JWT tokens after logout revoke active JWT tokens after logout GH-2570 --- .../services-bearer-asym-token-verifier.ts | 8 +++- .../services-bearer-token-verify.provider.ts | 10 ++++- .../utils/revoked-token-checker.util.ts | 44 +++++++++++++++++++ 3 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 packages/core/src/components/bearer-verifier/providers/utils/revoked-token-checker.util.ts diff --git a/packages/core/src/components/bearer-verifier/providers/services-bearer-asym-token-verifier.ts b/packages/core/src/components/bearer-verifier/providers/services-bearer-asym-token-verifier.ts index d4ef494843..062df5b81e 100644 --- a/packages/core/src/components/bearer-verifier/providers/services-bearer-asym-token-verifier.ts +++ b/packages/core/src/components/bearer-verifier/providers/services-bearer-asym-token-verifier.ts @@ -14,21 +14,27 @@ import { } from 'loopback4-authentication'; import moment from 'moment-timezone'; import * as jose from 'node-jose'; -import {JwtKeysRepository} from '../../../repositories'; +import {JwtKeysRepository, RevokedTokenRepository} from '../../../repositories'; import {ILogger, LOGGER} from '../../logger-extension'; import {IAuthUserWithPermissions} from '../keys'; +import {checkIfTokenRevoked} from './utils/revoked-token-checker.util'; export class ServicesBearerAsymmetricTokenVerifyProvider implements Provider { constructor( @inject(LOGGER.LOGGER_INJECT) public logger: ILogger, @repository(JwtKeysRepository) public jwtKeysRepo: JwtKeysRepository, + @repository(RevokedTokenRepository) + public revokedTokenRepo: RevokedTokenRepository, @inject(AuthenticationBindings.USER_MODEL, {optional: true}) public authUserModel?: Constructor, ) {} value(): VerifyFunction.BearerFn { return async (token: string) => { + // Check if token has been revoked + await checkIfTokenRevoked(token, this.revokedTokenRepo, this.logger); + let user: IAuthUserWithPermissions; try { diff --git a/packages/core/src/components/bearer-verifier/providers/services-bearer-token-verify.provider.ts b/packages/core/src/components/bearer-verifier/providers/services-bearer-token-verify.provider.ts index fb0e5fdb43..93c9396a4b 100644 --- a/packages/core/src/components/bearer-verifier/providers/services-bearer-token-verify.provider.ts +++ b/packages/core/src/components/bearer-verifier/providers/services-bearer-token-verify.provider.ts @@ -1,8 +1,9 @@ -// Copyright (c) 2023 Sourcefuse Technologies +// Copyright (c) 2023 Sourcefuse Technologies // // This software is released under the MIT License. // https://opensource.org/licenses/MIT import {Constructor, inject, Provider} from '@loopback/context'; +import {repository} from '@loopback/repository'; import {HttpErrors} from '@loopback/rest'; import {verify} from 'jsonwebtoken'; import { @@ -12,18 +13,25 @@ import { VerifyFunction, } from 'loopback4-authentication'; import moment from 'moment-timezone'; +import {RevokedTokenRepository} from '../../../repositories'; import {ILogger, LOGGER} from '../../logger-extension'; import {IAuthUserWithPermissions} from '../keys'; +import {checkIfTokenRevoked} from './utils/revoked-token-checker.util'; export class ServicesBearerTokenVerifyProvider implements Provider { constructor( @inject(LOGGER.LOGGER_INJECT) public logger: ILogger, + @repository(RevokedTokenRepository) + public revokedTokenRepo: RevokedTokenRepository, @inject(AuthenticationBindings.USER_MODEL, {optional: true}) public authUserModel?: Constructor, ) {} value(): VerifyFunction.BearerFn { return async (token: string) => { + // Check if token has been revoked + await checkIfTokenRevoked(token, this.revokedTokenRepo, this.logger); + let user: IAuthUserWithPermissions; try { diff --git a/packages/core/src/components/bearer-verifier/providers/utils/revoked-token-checker.util.ts b/packages/core/src/components/bearer-verifier/providers/utils/revoked-token-checker.util.ts new file mode 100644 index 0000000000..248c9589a8 --- /dev/null +++ b/packages/core/src/components/bearer-verifier/providers/utils/revoked-token-checker.util.ts @@ -0,0 +1,44 @@ +// Copyright (c) 2023 Sourcefuse Technologies +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT +import {HttpErrors} from '@loopback/rest'; +import {RevokedTokenRepository} from '../../../../repositories'; +import {AuthenticateErrorKeys} from '../../../../enums/auth-error-keys.enum'; +import {ILogger} from '../../../../components/logger-extension'; + +/** + * Checks if a token has been revoked and throws an error if it has. + * + * This function queries the RevokedTokenRepository to determine if the given token + * has been revoked. If the token is found in the revoked list, an Unauthorized + * error is thrown, preventing the use of previously logged-out tokens. + * + * @param token - The JWT token to check for revocation + * @param revokedTokenRepo - The repository to check for revoked tokens + * @param logger - Logger instance for security and error logging + * @throws {HttpErrors.Unauthorized} When the token has been revoked + */ +export async function checkIfTokenRevoked( + token: string, + revokedTokenRepo: RevokedTokenRepository, + logger: ILogger, +): Promise { + try { + const isRevoked = await revokedTokenRepo.get(token); + if (isRevoked?.token) { + logger.warn(`[SECURITY] Attempt to use revoked token detected`); + throw new HttpErrors.Unauthorized(AuthenticateErrorKeys.TokenRevoked); + } + } catch (error) { + // Re-throw HTTP errors (like our TokenRevoked error) + if (HttpErrors.HttpError.prototype.isPrototypeOf(error)) { + throw error; + } + // Log but don't fail on repository errors to allow graceful degradation + logger.error( + `[AUTH] Revoked token repository error during token verification.`, + error, + ); + } +}