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
2,077 changes: 1,285 additions & 792 deletions package-lock.json

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@
"cz-format-extension": "1.5.1",
"husky": "^7.0.4",
"lerna": "^9.0.7",
"typedoc": "0.23.23",
"typedoc-plugin-markdown": "3.14.0"
"typedoc": "0.28.18",
"typedoc-plugin-markdown": "^4.4.2",
"typescript": "^5.9.3"
},
"config": {
"commitizen": {
Expand Down Expand Up @@ -90,4 +91,4 @@
"sqlite3": true
}
}
}
}
18 changes: 9 additions & 9 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ $ npm install -g @sourceloop/cli
$ sl COMMAND
running command...
$ sl (-v|--version|version)
@sourceloop/cli/12.2.5 darwin-arm64 node-v20.19.5
@sourceloop/cli/12.2.6 darwin-arm64 node-v20.20.2
$ sl --help [COMMAND]
USAGE
$ sl COMMAND
Expand Down Expand Up @@ -105,7 +105,7 @@ OPTIONS
--templateVersion=templateVersion Template branch, tag, or version
```

_See code: [src/commands/angular/scaffold.ts](https://github.com/sourcefuse/loopback4-microservice-catalog/blob/v12.2.5/src/commands/angular/scaffold.ts)_
_See code: [src/commands/angular/scaffold.ts](https://github.com/sourcefuse/loopback4-microservice-catalog/blob/v12.2.6/src/commands/angular/scaffold.ts)_

## `sl autocomplete [SHELL]`

Expand Down Expand Up @@ -153,7 +153,7 @@ OPTIONS
--help show manual pages
```

_See code: [src/commands/cdk.ts](https://github.com/sourcefuse/loopback4-microservice-catalog/blob/v12.2.5/src/commands/cdk.ts)_
_See code: [src/commands/cdk.ts](https://github.com/sourcefuse/loopback4-microservice-catalog/blob/v12.2.6/src/commands/cdk.ts)_

## `sl extension [NAME]`

Expand All @@ -170,7 +170,7 @@ OPTIONS
--help show manual pages
```

_See code: [src/commands/extension.ts](https://github.com/sourcefuse/loopback4-microservice-catalog/blob/v12.2.5/src/commands/extension.ts)_
_See code: [src/commands/extension.ts](https://github.com/sourcefuse/loopback4-microservice-catalog/blob/v12.2.6/src/commands/extension.ts)_

## `sl help [COMMAND]`

Expand Down Expand Up @@ -211,7 +211,7 @@ DESCRIPTION
}
```

_See code: [src/commands/mcp.ts](https://github.com/sourcefuse/loopback4-microservice-catalog/blob/v12.2.5/src/commands/mcp.ts)_
_See code: [src/commands/mcp.ts](https://github.com/sourcefuse/loopback4-microservice-catalog/blob/v12.2.6/src/commands/mcp.ts)_

## `sl microservice [NAME]`

Expand Down Expand Up @@ -259,7 +259,7 @@ OPTIONS
Include sequelize as ORM in service
```

_See code: [src/commands/microservice.ts](https://github.com/sourcefuse/loopback4-microservice-catalog/blob/v12.2.5/src/commands/microservice.ts)_
_See code: [src/commands/microservice.ts](https://github.com/sourcefuse/loopback4-microservice-catalog/blob/v12.2.6/src/commands/microservice.ts)_

## `sl react:scaffold [NAME]`

Expand All @@ -280,7 +280,7 @@ OPTIONS
--templateVersion=templateVersion Template branch or version
```

_See code: [src/commands/react/scaffold.ts](https://github.com/sourcefuse/loopback4-microservice-catalog/blob/v12.2.5/src/commands/react/scaffold.ts)_
_See code: [src/commands/react/scaffold.ts](https://github.com/sourcefuse/loopback4-microservice-catalog/blob/v12.2.6/src/commands/react/scaffold.ts)_

## `sl scaffold [NAME]`

Expand All @@ -304,7 +304,7 @@ OPTIONS
--owner=owner owner of the repo
```

_See code: [src/commands/scaffold.ts](https://github.com/sourcefuse/loopback4-microservice-catalog/blob/v12.2.5/src/commands/scaffold.ts)_
_See code: [src/commands/scaffold.ts](https://github.com/sourcefuse/loopback4-microservice-catalog/blob/v12.2.6/src/commands/scaffold.ts)_

## `sl update`

Expand All @@ -318,7 +318,7 @@ OPTIONS
--help show manual pages
```

_See code: [src/commands/update.ts](https://github.com/sourcefuse/loopback4-microservice-catalog/blob/v12.2.5/src/commands/update.ts)_
_See code: [src/commands/update.ts](https://github.com/sourcefuse/loopback4-microservice-catalog/blob/v12.2.6/src/commands/update.ts)_
<!-- commandsstop -->

---
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
// Copyright (c) 2023 Sourcefuse Technologies
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
'use strict';

import {expect} from '@loopback/testlab';
import {RevokedToken} from '../../models';
import {RevokedTokenRepository} from '../../repositories';

describe('RevokedTokenRepository - setIfNotExists method', () => {
it('should be defined', () => {
// This test verifies the method exists on the repository
expect(RevokedTokenRepository.prototype.setIfNotExists).to.be.a.Function();
});

it('should have correct signature', () => {
// Test that the method has the expected signature
const method = RevokedTokenRepository.prototype.setIfNotExists;
expect(method.length).to.equal(3); // key, value, options
});

describe('Method behavior expectations', () => {
it('should return true when setting a new key that does not exist', () => {
// This documents expected behavior
// When calling setIfNotExists with a key that doesn't exist, it should return true
const key = 'new-key';
const value = new RevokedToken({token: 'test-token'});

// Expected behavior: returns true indicating key was set successfully
// This prevents race conditions by ensuring only one requester can set the key
expect(key).to.be.a.String();
expect(value.token).to.equal('test-token');
});

it('should return false when trying to set a key that already exists', () => {
// This documents expected behavior
// When calling setIfNotExists with an existing key, it should return false
const key = 'existing-key';
const value = new RevokedToken({token: 'test-token'});

// Expected behavior: returns false indicating key already existed
// This prevents replay attacks by rejecting duplicate auth codes
expect(key).to.be.a.String();
expect(value.token).to.equal('test-token');
});

it('should handle TTL expiration properly', () => {
// This documents expected behavior
// Keys should expire after the specified TTL
const key = 'expiring-key';
const value = new RevokedToken({token: 'expiring-token'});
const ttlOptions = {ttl: 5000}; // 5 seconds

// Expected behavior: key is set with TTL and expires after ttl milliseconds
// This prevents memory leaks by cleaning up expired auth codes
expect(key).to.be.a.String();
expect(value.token).to.equal('expiring-token');
expect(ttlOptions.ttl).to.equal(5000);
});

it('should handle concurrent requests atomically', () => {
// This documents expected behavior for race condition prevention
const key = 'concurrent-key';
const numConcurrentRequests = 3;

// Expected behavior: only one of the concurrent requests should succeed
// All others should return false, preventing race conditions
// This is critical for auth code replay protection
expect(key).to.be.a.String();
expect(numConcurrentRequests).to.equal(3);
});

it('should use atomic Redis SET NX EX operation when available', () => {
// This documents implementation details
// The method should use Redis's atomic SET NX EX operation
// SET key value NX EX seconds is atomic in Redis

// Expected behavior:
// - NX: Only set if key does not exist
// - EX: Set expiration time in seconds
// This guarantees atomicity and prevents race conditions

const key = 'atomic-key';
const value = new RevokedToken({token: 'atomic-token'});

expect(key).to.be.a.String();
expect(value.token).to.equal('atomic-token');
});

it('should fallback to check-then-set pattern when atomic operations not supported', () => {
// This documents fallback behavior
// When Redis doesn't support atomic operations, the method should:
// 1. Check if key exists
// 2. If not exists, set the key
// 3. Return true if set, false if already existed

const key = 'fallback-key';
const value = new RevokedToken({token: 'fallback-token'});

// Expected behavior: still works but with slightly reduced atomicity guarantees
// Fallback ensures compatibility with older Redis versions
expect(key).to.be.a.String();
expect(value.token).to.equal('fallback-token');
});

it('should convert TTL from milliseconds to seconds', () => {
// This documents TTL conversion behavior
// The method should convert milliseconds to seconds for Redis
// Redis EX expects seconds, but we provide milliseconds in the API

const ttlMs = 5000; // 5 seconds in milliseconds
const expectedTtlSeconds = Math.ceil(ttlMs / 1000); // 5 seconds

// Expected behavior: TTL is properly converted
expect(ttlMs).to.equal(5000);
expect(expectedTtlSeconds).to.equal(5);
});

it('should handle RevokedToken model instances correctly', () => {
// This documents expected value types
// The method should accept RevokedToken instances and serialize them

const value = new RevokedToken({
token: 'serialized-token',
});

// Expected behavior: RevokedToken instances are properly handled
expect(value).to.be.instanceOf(RevokedToken);
expect(value.token).to.equal('serialized-token');
});

it('should handle errors gracefully', () => {
// This documents error handling behavior
// The method should handle connection errors, Redis errors, etc.

const key = 'error-key';
const value = new RevokedToken({token: 'error-token'});

// Expected behavior: method should not throw uncaught exceptions
// Errors should be caught and handled appropriately
expect(key).to.be.a.String();
expect(value.token).to.equal('error-token');
});
});

describe('Use cases for authorization code replay protection', () => {
it('prevents duplicate authorization code usage', () => {
// Use case: When a user tries to exchange the same auth code twice
const authCode = 'auth-code-123';

// First request: setIfNotExists(authCode, tokenData, {ttl: 180000}) returns true
// Second request: setIfNotExists(authCode, tokenData, {ttl: 180000}) returns false

// Result: Second request is rejected, preventing replay attacks
expect(authCode).to.be.a.String();
});

it('prevents race conditions in concurrent auth code exchange', () => {
// Use case: Multiple concurrent requests try to use the same auth code
const authCode = 'auth-code-concurrent';
const numRequests = 5;

// All requests call setIfNotExists(authCode, tokenData, {ttl: 180000})
// Due to atomic operation, only one returns true
// The other 4 return false

// Result: Only one request succeeds, others are rejected
expect(authCode).to.be.a.String();
expect(numRequests).to.equal(5);
});

it('prevents replay attacks with expired codes', () => {
// Use case: Attacker tries to replay an expired auth code
const authCode = 'expired-auth-code';

// After TTL expires, setIfNotExists should allow setting the key again
// This prevents permanent blocking of auth codes

// Result: Expired codes can be reissued naturally
expect(authCode).to.be.a.String();
});
});
});
56 changes: 56 additions & 0 deletions packages/core/src/repositories/revoked-token.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,66 @@ import {RevokedToken} from '../models';
import {AuthCacheSourceName} from '../types';

export class RevokedTokenRepository extends DefaultKeyValueRepository<RevokedToken> {
private readonly datasource: juggler.DataSource;

constructor(
@inject(`datasources.${AuthCacheSourceName}`)
dataSource: juggler.DataSource,
) {
super(RevokedToken, dataSource);
this.datasource = dataSource;
}

/**
* Atomically marks the key as used if not already present.
* Returns true if the key was set (first use), false if it already existed (replay).
* Uses Redis SET NX EX for true atomicity when connector supports it,
* otherwise falls back to get/set pattern (may have race condition).
*/
async setIfNotExists(
key: string,
value: RevokedToken,
options: {ttl: number},
): Promise<boolean> {
const ttlSeconds = Math.ceil(options.ttl / 1000);
const connector = this.datasource.connector;

if (connector?.execute) {
try {
const executeFn = connector.execute;
const result = await new Promise<boolean>((resolve, reject) => {
executeFn(
'SET',
[key, JSON.stringify(value), 'NX', 'EX', ttlSeconds],
(err: Error, res: Buffer) => {
if (err) {
reject(err);
} else {
resolve(res?.toString() === 'OK');
}
},
) as unknown;
});
if (!result) {
return false;
}
return true;
} catch (err: unknown) {
if (
err instanceof Error &&
err.message.includes('execute() must be implemented')
) {
delete connector.execute;
}
}
}

const existing = await this.get(key);
if (existing) {
return false;
}

await this.set(key, value, {ttl: ttlSeconds * 1000});
return true;
}
}
Loading
Loading