From c59636a1aca67be7d6732d281cec307ed456678b Mon Sep 17 00:00:00 2001 From: Stefanos Anagnostou Date: Mon, 23 Sep 2024 15:02:05 +0300 Subject: [PATCH] fix(backend): Replace `session-token-outdated` error with more specific errors (#4205) --- .changeset/ninety-comics-scream.md | 5 ++++ packages/backend/src/errors.ts | 1 + packages/backend/src/jwt/assertions.ts | 2 +- .../src/tokens/__tests__/request.test.ts | 6 ++--- packages/backend/src/tokens/authStatus.ts | 5 +++- packages/backend/src/tokens/request.ts | 23 ++++++++++++++++--- 6 files changed, 34 insertions(+), 8 deletions(-) create mode 100644 .changeset/ninety-comics-scream.md diff --git a/.changeset/ninety-comics-scream.md b/.changeset/ninety-comics-scream.md new file mode 100644 index 0000000000..e0b493c282 --- /dev/null +++ b/.changeset/ninety-comics-scream.md @@ -0,0 +1,5 @@ +--- +"@clerk/backend": patch +--- + +Improve debugging error reasons. diff --git a/packages/backend/src/errors.ts b/packages/backend/src/errors.ts index 111cf4d8ec..90cfdc343a 100644 --- a/packages/backend/src/errors.ts +++ b/packages/backend/src/errors.ts @@ -13,6 +13,7 @@ export const TokenVerificationErrorReason = { TokenInvalidAuthorizedParties: 'token-invalid-authorized-parties', TokenInvalidSignature: 'token-invalid-signature', TokenNotActiveYet: 'token-not-active-yet', + TokenIatInTheFuture: 'token-iat-in-the-future', TokenVerificationFailed: 'token-verification-failed', InvalidSecretKey: 'secret-key-invalid', LocalJWKMissing: 'jwk-local-missing', diff --git a/packages/backend/src/jwt/assertions.ts b/packages/backend/src/jwt/assertions.ts index f91e9b2636..4153de625f 100644 --- a/packages/backend/src/jwt/assertions.ts +++ b/packages/backend/src/jwt/assertions.ts @@ -162,7 +162,7 @@ export const assertIssuedAtClaim = (iat: number | undefined, clockSkewInMs: numb const postIssued = issuedAtDate.getTime() > currentDate.getTime() + clockSkewInMs; if (postIssued) { throw new TokenVerificationError({ - reason: TokenVerificationErrorReason.TokenNotActiveYet, + reason: TokenVerificationErrorReason.TokenIatInTheFuture, message: `JWT issued at date claim (iat) is in the future. Issued at date: ${issuedAtDate.toUTCString()}; Current date: ${currentDate.toUTCString()};`, }); } diff --git a/packages/backend/src/tokens/__tests__/request.test.ts b/packages/backend/src/tokens/__tests__/request.test.ts index 73a68c74ba..e72c8904b1 100644 --- a/packages/backend/src/tokens/__tests__/request.test.ts +++ b/packages/backend/src/tokens/__tests__/request.test.ts @@ -238,7 +238,7 @@ export default (QUnit: QUnit) => { const requestState = await authenticateRequest(mockRequestWithHeaderAuth(), mockOptions()); - assertHandshake(assert, requestState, { reason: AuthErrorReason.SessionTokenOutdated }); + assertHandshake(assert, requestState, { reason: AuthErrorReason.SessionTokenExpired }); assert.strictEqual(requestState.toAuth(), null); }); @@ -487,7 +487,7 @@ export default (QUnit: QUnit) => { mockOptions(), ); - assertHandshake(assert, requestState, { reason: AuthErrorReason.SessionTokenOutdated }); + assertHandshake(assert, requestState, { reason: AuthErrorReason.SessionTokenIATBeforeClientUAT }); assert.equal(requestState.message, ''); assert.strictEqual(requestState.toAuth(), null); }); @@ -554,7 +554,7 @@ export default (QUnit: QUnit) => { mockOptions(), ); - assertHandshake(assert, requestState, { reason: AuthErrorReason.SessionTokenOutdated }); + assertHandshake(assert, requestState, { reason: AuthErrorReason.SessionTokenExpired }); assert.true(/^JWT is expired/.test(requestState.message || '')); assert.strictEqual(requestState.toAuth(), null); }); diff --git a/packages/backend/src/tokens/authStatus.ts b/packages/backend/src/tokens/authStatus.ts index 78a7de9c12..ec836e6a85 100644 --- a/packages/backend/src/tokens/authStatus.ts +++ b/packages/backend/src/tokens/authStatus.ts @@ -64,7 +64,10 @@ export const AuthErrorReason = { SatelliteCookieNeedsSyncing: 'satellite-needs-syncing', SessionTokenAndUATMissing: 'session-token-and-uat-missing', SessionTokenMissing: 'session-token-missing', - SessionTokenOutdated: 'session-token-outdated', + SessionTokenExpired: 'session-token-expired', + SessionTokenIATBeforeClientUAT: 'session-token-iat-before-client-uat', + SessionTokenNotActiveYet: 'session-token-not-active-yet', + SessionTokenIatInTheFuture: 'session-token-iat-in-the-future', SessionTokenWithoutClientUAT: 'session-token-but-no-client-uat', UnexpectedError: 'unexpected-error', } as const; diff --git a/packages/backend/src/tokens/request.ts b/packages/backend/src/tokens/request.ts index 53c5c19f2d..9a3bb1257f 100644 --- a/packages/backend/src/tokens/request.ts +++ b/packages/backend/src/tokens/request.ts @@ -165,7 +165,8 @@ export async function authenticateRequest( if ( authenticateContext.instanceType === 'development' && (error?.reason === TokenVerificationErrorReason.TokenExpired || - error?.reason === TokenVerificationErrorReason.TokenNotActiveYet) + error?.reason === TokenVerificationErrorReason.TokenNotActiveYet || + error?.reason === TokenVerificationErrorReason.TokenIatInTheFuture) ) { error.tokenCarrier = 'cookie'; // This probably means we're dealing with clock skew @@ -456,7 +457,7 @@ ${error.getFullMessage()}`, } if (decodeResult.payload.iat < authenticateContext.clientUat) { - return handleMaybeHandshakeStatus(authenticateContext, AuthErrorReason.SessionTokenOutdated, ''); + return handleMaybeHandshakeStatus(authenticateContext, AuthErrorReason.SessionTokenIATBeforeClientUAT, ''); } try { @@ -502,12 +503,13 @@ ${error.getFullMessage()}`, const reasonToHandshake = [ TokenVerificationErrorReason.TokenExpired, TokenVerificationErrorReason.TokenNotActiveYet, + TokenVerificationErrorReason.TokenIatInTheFuture, ].includes(err.reason); if (reasonToHandshake) { return handleMaybeHandshakeStatus( authenticateContext, - AuthErrorReason.SessionTokenOutdated, + convertTokenVerificationErrorReasonToAuthErrorReason(err.reason), err.getFullMessage(), undefined, refreshError, @@ -531,3 +533,18 @@ export const debugRequestState = (params: RequestState) => { const { isSignedIn, proxyUrl, reason, message, publishableKey, isSatellite, domain } = params; return { isSignedIn, proxyUrl, reason, message, publishableKey, isSatellite, domain }; }; + +const convertTokenVerificationErrorReasonToAuthErrorReason = ( + reason: TokenVerificationErrorReason, +): AuthErrorReason => { + switch (reason) { + case TokenVerificationErrorReason.TokenExpired: + return AuthErrorReason.SessionTokenExpired; + case TokenVerificationErrorReason.TokenNotActiveYet: + return AuthErrorReason.SessionTokenNotActiveYet; + case TokenVerificationErrorReason.TokenIatInTheFuture: + return AuthErrorReason.SessionTokenIatInTheFuture; + default: + return AuthErrorReason.UnexpectedError; + } +};