diff --git a/packages/core/src/components/bearer-verifier/providers/facades-bearer-asym-token-verify.provider.ts b/packages/core/src/components/bearer-verifier/providers/facades-bearer-asym-token-verify.provider.ts index 9633fc44c6..97f9fb54ef 100644 --- a/packages/core/src/components/bearer-verifier/providers/facades-bearer-asym-token-verify.provider.ts +++ b/packages/core/src/components/bearer-verifier/providers/facades-bearer-asym-token-verify.provider.ts @@ -20,6 +20,7 @@ import { } from '../../../repositories'; import {ILogger, LOGGER} from '../../logger-extension'; import {IAuthUserWithPermissions} from '../keys'; +import {checkIfTokenRevoked} from './utils/revoked-token-checker.util'; export class FacadesBearerAsymmetricTokenVerifyProvider implements Provider { constructor( @@ -43,7 +44,12 @@ export class FacadesBearerAsymmetricTokenVerifyProvider implements Provider { - await this._checkIfTokenRevoked(token); + // Check if token has been revoked (fail-closed: errors propagate and deny request) + await checkIfTokenRevoked( + token, + this.revokedTokenRepository, + this.logger, + ); let user = await this._verifyTokenAndGetUser(token); this._checkPasswordExpiry(user); try { @@ -90,27 +96,6 @@ export class FacadesBearerAsymmetricTokenVerifyProvider implements Provider { - try { - const isRevoked = await this.revokedTokenRepository.get(token); - if (isRevoked?.token) { - throw new HttpErrors.Unauthorized('TokenRevoked'); - } - } catch (error) { - if (HttpErrors.HttpError.prototype.isPrototypeOf(error)) { - throw error; - } - this.logger.error('Revoked token repository not available !'); - } - } - /** * The function `_verifyTokenAndGetUser` verifies a token, decodes it, retrieves the corresponding * key, and then verifies the token's authenticity using the key. diff --git a/packages/core/src/components/bearer-verifier/providers/facades-bearer-token-verify.provider.ts b/packages/core/src/components/bearer-verifier/providers/facades-bearer-token-verify.provider.ts index 5f99c98fb1..313d8416e0 100644 --- a/packages/core/src/components/bearer-verifier/providers/facades-bearer-token-verify.provider.ts +++ b/packages/core/src/components/bearer-verifier/providers/facades-bearer-token-verify.provider.ts @@ -17,6 +17,7 @@ import moment from 'moment'; import {RevokedTokenRepository} from '../../../repositories'; import {ILogger, LOGGER} from '../../logger-extension'; import {IAuthUserWithPermissions} from '../keys'; +import {checkIfTokenRevoked} from './utils/revoked-token-checker.util'; export class FacadesBearerTokenVerifyProvider implements Provider { constructor( @@ -31,23 +32,19 @@ export class FacadesBearerTokenVerifyProvider implements Provider { - try { - const isRevoked = await this.revokedTokenRepository.get(token); - if (isRevoked?.token) { - throw new HttpErrors.Unauthorized('TokenRevoked'); - } - } catch (error) { - if (HttpErrors.HttpError.prototype.isPrototypeOf(error)) { - throw error; - } - this.logger.error('Revoked token repository not available !'); - } + // Check if token has been revoked (fail-closed: errors propagate and deny request) + await checkIfTokenRevoked( + token, + this.revokedTokenRepository, + this.logger, + ); let user: IAuthUserWithPermissions; try { 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..261d7e11ac --- /dev/null +++ b/packages/core/src/components/bearer-verifier/providers/utils/revoked-token-checker.util.ts @@ -0,0 +1,39 @@ +// 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. + * + * **Security posture (fail-closed)**: If the revoked token repository is unavailable + * (Redis down, timeout, connection errors), the error propagates and the request is + * denied. This ensures that logout always takes effect - if we cannot verify a token + * is not revoked, we reject it. This matches the established pattern from + * authentication-service's bearer-token-verify.provider.ts. + * + * @param token - The JWT token to check for revocation + * @param revokedTokenRepo - The repository to check for revoked tokens + * @param logger - Logger instance for security logging + * @throws {HttpErrors.Unauthorized} When the token has been revoked + * @throws When the revoked token repository is unavailable + */ +export async function checkIfTokenRevoked( + token: string, + revokedTokenRepo: RevokedTokenRepository, + logger: ILogger, +): Promise { + const isRevoked = await revokedTokenRepo.get(token); + if (isRevoked?.token) { + logger.warn(`[SECURITY] Attempt to use revoked token detected`); + throw new HttpErrors.Unauthorized(AuthenticateErrorKeys.TokenRevoked); + } +} diff --git a/services/task-service/src/__tests__/fixtures/test-helper.ts b/services/task-service/src/__tests__/fixtures/test-helper.ts index c4e8de7170..5885807168 100644 --- a/services/task-service/src/__tests__/fixtures/test-helper.ts +++ b/services/task-service/src/__tests__/fixtures/test-helper.ts @@ -38,6 +38,12 @@ export async function setupApplication(loggerStub?: ILogger) { }); app.dataSource(AuthenticationDbDataSource); + app.bind(`datasources.${AuthCacheSourceName}`).to( + new juggler.DataSource({ + name: AuthCacheSourceName, + connector: 'kv-memory', + }), + ); app.bind(`datasources.config.${AuthCacheSourceName}`).to({ name: 'redis', connector: 'kv-memory', diff --git a/services/user-tenant-service/src/__tests__/acceptance/test-helper.ts b/services/user-tenant-service/src/__tests__/acceptance/test-helper.ts index 2f6174ce7f..308bd953dd 100644 --- a/services/user-tenant-service/src/__tests__/acceptance/test-helper.ts +++ b/services/user-tenant-service/src/__tests__/acceptance/test-helper.ts @@ -8,7 +8,10 @@ import { givenHttpServerConfig, } from '@loopback/testlab'; import {UserTenantServiceApplication} from '../../application'; -import {AuthenticationDbDataSource} from '../datasources'; +import { + AuthenticationCacheDbDataSource, + AuthenticationDbDataSource, +} from '../datasources'; export async function setupApplication(): Promise { const restConfig = givenHttpServerConfig({ @@ -28,7 +31,12 @@ export async function setupApplication(): Promise { name: 'AuthDB', connector: 'memory', }); + app.bind('datasources.AuthCache').to({ + name: 'AuthCache', + connector: 'kv-memory', + }); app.dataSource(AuthenticationDbDataSource); + app.dataSource(AuthenticationCacheDbDataSource); await app.boot(); await app.start(); diff --git a/services/user-tenant-service/src/__tests__/datasources/cache.datasource.ts b/services/user-tenant-service/src/__tests__/datasources/cache.datasource.ts index d9828ed9d8..8a1104435d 100644 --- a/services/user-tenant-service/src/__tests__/datasources/cache.datasource.ts +++ b/services/user-tenant-service/src/__tests__/datasources/cache.datasource.ts @@ -12,7 +12,7 @@ import {juggler} from '@loopback/repository'; const config = { name: 'AuthCache', - connector: 'memory', + connector: 'kv-memory', localStorage: '', file: '', };