Skip to content
Open
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
5,289 changes: 3,483 additions & 1,806 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@
"@types/js-yaml": "^4.0.5",
"js-yaml": "^4.1.0",
"lodash": "^4.17.21",
"@sourceloop/core": "^20.0.0",
"@sourceloop/core": "^21.0.0",
"@sourceloop/cache": "^6.0.0",
"@sourceloop/feature-toggle": "^6.0.0",
"@sourceloop/audit-service": "^19.0.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<VerifyFunction.BearerFn> {
constructor(
Expand All @@ -43,7 +44,12 @@ export class FacadesBearerAsymmetricTokenVerifyProvider implements Provider<Veri
*/
value(): VerifyFunction.BearerFn {
return async (token: string, req?: Request) => {
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 {
Expand Down Expand Up @@ -90,27 +96,6 @@ export class FacadesBearerAsymmetricTokenVerifyProvider implements Provider<Veri
};
}

/**
* The function `_checkIfTokenRevoked` checks if a token is revoked and throws an error if it is.
* @param {string} token - The `token` parameter in the `_checkIfTokenRevoked` function is a string
* that represents the token being checked for revocation. This token is used to query the
* `revokedTokenRepository` to determine if it has been revoked. If the token is found to be revoked,
* an `
*/
private async _checkIfTokenRevoked(token: string): Promise<void> {
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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<VerifyFunction.BearerFn> {
constructor(
Expand All @@ -31,23 +32,19 @@ export class FacadesBearerTokenVerifyProvider implements Provider<VerifyFunction
* The function verifies a bearer token, checks for token revocation, expiration, and password
* expiry, and returns the authenticated user.
* @returns The `value()` function returns a BearerFn function that verifies a token. Inside the
* function, it first checks if the token is revoked, then verifies the token using a JWT secret key.
* If the token is valid, it checks for password expiry and returns either an instance of
* `authUserModel` or the user object based on the availability of `authUserModel`.
* function, it first checks if the token is revoked using the shared utility (fail-closed: errors
* propagate and deny request), then verifies the token using a JWT secret key. If the token is
* valid, it checks for password expiry and returns either an instance of `authUserModel` or the
* user object based on the availability of `authUserModel`.
*/
value(): VerifyFunction.BearerFn {
return async (token: string, req?: Request) => {
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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<VerifyFunction.BearerFn> {
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<EntityWithIdentifier & IAuthUser>,
) {}

value(): VerifyFunction.BearerFn {
return async (token: string) => {
// Check if token has been revoked
await checkIfTokenRevoked(token, this.revokedTokenRepo, this.logger);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where are we revoking this token actually ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sir the token is revoked using the existing RevokedTokenRepository

on logout (idp-login.service.ts) : token is stored via revokedTokensRepo.set()

on api request (services-bearer-asym-token-verifier.ts) : token is checked via checkIfTokenRevoked() which calls revokedTokenRepo.get(token) and throws TokenRevoked error if found

The same mechanism that the authentication service and facade services already use - we just added the same check to the service-level verifiers which were missing it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the explanation — agreed that the set() side is the existing logout code and this PR adds the missing get() check on the service verifiers. Two things worth resolving before this goes in, both raised inline:

  1. The RevokedTokenRepository injection on these service verifiers is required, which forces an AuthCache datasource onto every type: service consumer — that's a backward-incompatible change (the reason the two test-helpers needed patching in this same PR).
  2. The check itself fails open on any store error, so during an AuthCache/Redis outage revoked tokens are accepted — which is the opposite of what a revocation control should do, and inconsistent with the auth-service verifier that fails closed.


let user: IAuthUserWithPermissions;

try {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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<VerifyFunction.BearerFn> {
constructor(
@inject(LOGGER.LOGGER_INJECT) public logger: ILogger,
@repository(RevokedTokenRepository)
public revokedTokenRepo: RevokedTokenRepository,

@rohit-sourcefuse rohit-sourcefuse Jul 21, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@repository(RevokedTokenRepository) here is a required injection, and that repository hard-requires the AuthCache datasource (@inject('datasources.AuthCache')). BearerVerifierComponent selects these two providers for every type: BearerVerifierType.service consumer. Before this PR the service path never injected this repo, so the AuthCache datasource was never resolved on that path; now it is resolved when the provider is constructed, so any consumer without an AuthCache datasource breaks with a ResolutionError on every authenticated request. The test-helper edits in this PR (task-service, user-tenant-service) are that same breakage being patched for two in-repo apps.

One correction to flag on the fix: making the injection {optional: true} will NOT prevent this. BearerVerifierComponent already binds RevokedTokenRepository unconditionally (component.ts this.repositories = [RevokedTokenRepository, ...]), so the repositories.RevokedTokenRepository binding exists in every consumer. optional only yields undefined when the binding key is not found — here it is found, and resolving it then fails one level deeper on the missing datasources.AuthCache. That ResolutionError propagates regardless of optional.

So this is effectively a new required datasource dependency for all service-type consumers. Two honest options:

  1. Treat it as a documented breaking change (major version bump + migration note): consumers must add an AuthCache datasource. This mirrors the facade path, which already requires it. The scheduler-example sandbox datasource is the template — connector: 'kv-redis' reading datasources.config.AuthCache.

  2. If backward compatibility must be preserved, the optionality has to live at the datasource level, not the repository level — e.g. inject @inject('datasources.AuthCache', {optional: true}) and skip the revocation check when it's absent. That keeps existing consumers booting, at the cost of silently no-op'ing revocation where the cache isn't configured (which should then be logged/documented).

Same applies to services-bearer-asym-token-verifier.ts.

@inject(AuthenticationBindings.USER_MODEL, {optional: true})
public authUserModel?: Constructor<EntityWithIdentifier & IAuthUser>,
) {}

value(): VerifyFunction.BearerFn {
return async (token: string) => {
// Check if token has been revoked
await checkIfTokenRevoked(token, this.revokedTokenRepo, this.logger);

let user: IAuthUserWithPermissions;

try {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<void> {
const isRevoked = await revokedTokenRepo.get(token);
if (isRevoked?.token) {
logger.warn(`[SECURITY] Attempt to use revoked token detected`);
throw new HttpErrors.Unauthorized(AuthenticateErrorKeys.TokenRevoked);
}
}
Loading
Loading