diff --git a/packages/plugins/@nocobase/plugin-auth/src/constants.ts b/packages/plugins/@nocobase/plugin-auth/src/constants.ts index 001b6891dd..ac14f05e5e 100644 --- a/packages/plugins/@nocobase/plugin-auth/src/constants.ts +++ b/packages/plugins/@nocobase/plugin-auth/src/constants.ts @@ -11,3 +11,4 @@ export const tokenPolicyRecordKey = 'token-policy-config'; export const tokenPolicyCacheKey = 'auth:' + tokenPolicyRecordKey; export const tokenPolicyCollectionName = 'tokenControlConfig'; export const issuedTokensCollectionName = 'issuedTokens'; +export const RENEWED_JTI_CACHE_MS = 10000; diff --git a/packages/plugins/@nocobase/plugin-auth/src/server/__tests__/token-controller.test.ts b/packages/plugins/@nocobase/plugin-auth/src/server/__tests__/token-controller.test.ts index 38b374d9d2..9a6605a481 100644 --- a/packages/plugins/@nocobase/plugin-auth/src/server/__tests__/token-controller.test.ts +++ b/packages/plugins/@nocobase/plugin-auth/src/server/__tests__/token-controller.test.ts @@ -7,10 +7,11 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { AuthErrorCode, BaseAuth } from '@nocobase/auth'; +import { BaseAuth } from '@nocobase/auth'; import { Database, Model } from '@nocobase/database'; import { MockServer, createMockServer } from '@nocobase/test'; import { AuthErrorType } from '@nocobase/auth'; +import { RENEWED_JTI_CACHE_MS } from '../../constants'; function sleep(ms) { return new Promise((resolve) => { setTimeout(resolve, ms); @@ -161,19 +162,47 @@ describe('auth', () => { expect(checkedUser.id).toEqual(user.id); }); - it('when call renew token with same jti multiple times, only one resolved', async () => { + it('when call renew token with same jti multiple times within 10s, the result is same', async () => { const tokenInfo = await auth.tokenController.add({ userId: 1 }); const renewTasks = Array(15) .fill(null) .map(() => auth.tokenController.renew(tokenInfo.jti)); - const allSettled = await Promise.allSettled(renewTasks); - const successTasks = allSettled.filter((result) => result.status === 'fulfilled'); - expect(successTasks).toHaveLength(1); - const failedTasks = allSettled.filter( - (result) => result.status === 'rejected' && result.reason.code === AuthErrorCode.TOKEN_RENEW_FAILED, - ); - expect(failedTasks).toHaveLength(14); + const results = await Promise.all(renewTasks); + expect( + results.every((result) => result.jti === results[0].jti && result.issuedTime === results[0].issuedTime), + ).toBe(true); }); + + it('after JTI is renewed for 10s, any further renewal should fail.', async () => { + const tokenInfo = await auth.tokenController.add({ userId: 1 }); + await auth.tokenController.renew(tokenInfo.jti); + const renewedIntervals = [ + RENEWED_JTI_CACHE_MS - 1000, + RENEWED_JTI_CACHE_MS - 2000, + RENEWED_JTI_CACHE_MS + 1000, + RENEWED_JTI_CACHE_MS + 2000, + ]; + const renewTasks = renewedIntervals.map(async (interval) => { + try { + await sleep(interval); + const result = await auth.tokenController.renew(tokenInfo.jti); + return [interval, 'resolved', result]; + } catch (e) { + return [interval, 'rejected', e]; + } + }); + const results = await Promise.all(renewTasks); + expect( + results.every(([interval, status, result]) => { + if (interval < RENEWED_JTI_CACHE_MS) { + return status === 'resolved'; + } else { + return status === 'rejected'; + } + }), + ).toBe(true); + }); + it('use token policy tokenExpirationTime as token expirein', async () => { const config = await auth.tokenController.getConfig(); const { token } = await auth.signIn(); diff --git a/packages/plugins/@nocobase/plugin-auth/src/server/token-controller.ts b/packages/plugins/@nocobase/plugin-auth/src/server/token-controller.ts index c0c4975ed7..cd69929478 100644 --- a/packages/plugins/@nocobase/plugin-auth/src/server/token-controller.ts +++ b/packages/plugins/@nocobase/plugin-auth/src/server/token-controller.ts @@ -21,7 +21,12 @@ import { randomUUID } from 'crypto'; import ms from 'ms'; import Application from '@nocobase/server'; import Database, { Repository } from '@nocobase/database'; -import { issuedTokensCollectionName, tokenPolicyCollectionName, tokenPolicyRecordKey } from '../constants'; +import { + issuedTokensCollectionName, + tokenPolicyCollectionName, + tokenPolicyRecordKey, + RENEWED_JTI_CACHE_MS, +} from '../constants'; type TokenControlService = ITokenControlService; @@ -119,7 +124,7 @@ export class TokenController implements TokenControlService { ); if (count === 1) { - await this.cache.set(`jti-renewed-cahce:${jti}`, { jti: newId, issuedTime }, 20000); + await this.cache.set(`jti-renewed-cahce:${jti}`, { jti: newId, issuedTime }, RENEWED_JTI_CACHE_MS); this.logger.info('jti renewed', { oldJti: jti, newJti: newId, issuedTime }); return { jti: newId, issuedTime }; } else {