From 404ade3afe56e5ff7977ebaff29fc4806b4dc46d Mon Sep 17 00:00:00 2001 From: Om Singhal Date: Sat, 22 Aug 2026 20:24:57 -0400 Subject: [PATCH 1/2] fix(auth): keep stack writable on errors built from OAuth error responses getErrorFromOAuthErrorResponse copies the own properties of the original error onto the new Error with Object.defineProperty using writable false and enumerable true, and it adds stack to the list of copied keys. Every error raised from an STS or OAuth error response by external account credentials therefore carries a read only, enumerable stack, unlike a regular Error. Consumers commonly append causal context to error.stack. Compiled TypeScript runs in strict mode, so that assignment throws TypeError: Cannot assign to read only property 'stack', which replaces the real authentication failure. In Firestore the append happens inside a stream error handler, so the TypeError escapes as an uncaughtException instead of a rejected promise, and the underlying invalid_grant message is lost. Keep the copied stack writable and non enumerable, matching the shape of a normal Error, while leaving the other copied properties as they were. Add a unit test that checks the property descriptor and that appending to the stack in strict mode does not throw. Fixes #9155 --- .../src/auth/oauth2common.ts | 6 ++++-- .../test/test.oauth2common.ts | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/core/packages/google-auth-library-nodejs/src/auth/oauth2common.ts b/core/packages/google-auth-library-nodejs/src/auth/oauth2common.ts index 3b1de4419e11..cb91b18d2f77 100644 --- a/core/packages/google-auth-library-nodejs/src/auth/oauth2common.ts +++ b/core/packages/google-auth-library-nodejs/src/auth/oauth2common.ts @@ -255,8 +255,10 @@ export function getErrorFromOAuthErrorResponse( if (key !== 'message') { Object.defineProperty(newError, key, { value: (err as {} as {[index: string]: string})[key], - writable: false, - enumerable: true, + // Keep stack writable and non-enumerable, as on a regular Error, so + // callers can still append context to it. + writable: key === 'stack', + enumerable: key !== 'stack', }); } }); diff --git a/core/packages/google-auth-library-nodejs/test/test.oauth2common.ts b/core/packages/google-auth-library-nodejs/test/test.oauth2common.ts index c722b9c91646..b99f1f5eb0ae 100644 --- a/core/packages/google-auth-library-nodejs/test/test.oauth2common.ts +++ b/core/packages/google-auth-library-nodejs/test/test.oauth2common.ts @@ -500,4 +500,23 @@ describe('getErrorFromOAuthErrorResponse', () => { assert.strictEqual(actualError.name, expectedError.name); assert.strictEqual(actualError.stack, expectedError.stack); }); + + it('should keep the copied stack writable and non-enumerable', () => { + const originalError = new Error('Original error message'); + const resp = { + error: 'invalid_grant', + error_description: 'ID Token is stale to sign-in.', + }; + + const actualError = getErrorFromOAuthErrorResponse(resp, originalError); + const descriptor = Object.getOwnPropertyDescriptor(actualError, 'stack'); + assert.strictEqual(descriptor?.writable, true); + assert.strictEqual(descriptor?.enumerable, false); + // Consumers append causal context to error.stack; in strict mode that + // throws a TypeError when the property is read-only. + assert.doesNotThrow(() => { + actualError.stack += '\nCaused by: wrapped error'; + }); + assert.ok(actualError.stack?.endsWith('\nCaused by: wrapped error')); + }); }); From 097fffa8ee631628a6a94e7c2b349255fb8799b0 Mon Sep 17 00:00:00 2001 From: Om Singhal Date: Sun, 23 Aug 2026 00:22:54 -0400 Subject: [PATCH 2/2] fix(auth): make the copied stack explicitly configurable Review follow up for the OAuth error stack change. The defineProperty call in getErrorFromOAuthErrorResponse now sets configurable to true for stack, alongside the existing writable and enumerable attributes, instead of relying on the omitted attribute being kept from the stack that a fresh Error already owns. Other copied keys keep configurable false, which is what the omitted attribute already meant for them, so their descriptors are unchanged. The unit test now also asserts that the stack descriptor reports configurable true. --- .../google-auth-library-nodejs/src/auth/oauth2common.ts | 5 +++-- .../google-auth-library-nodejs/test/test.oauth2common.ts | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/core/packages/google-auth-library-nodejs/src/auth/oauth2common.ts b/core/packages/google-auth-library-nodejs/src/auth/oauth2common.ts index cb91b18d2f77..e8e02e2cc124 100644 --- a/core/packages/google-auth-library-nodejs/src/auth/oauth2common.ts +++ b/core/packages/google-auth-library-nodejs/src/auth/oauth2common.ts @@ -255,10 +255,11 @@ export function getErrorFromOAuthErrorResponse( if (key !== 'message') { Object.defineProperty(newError, key, { value: (err as {} as {[index: string]: string})[key], - // Keep stack writable and non-enumerable, as on a regular Error, so - // callers can still append context to it. + // Keep stack writable, configurable and non-enumerable, as on a + // regular Error, so callers can still append context to it. writable: key === 'stack', enumerable: key !== 'stack', + configurable: key === 'stack', }); } }); diff --git a/core/packages/google-auth-library-nodejs/test/test.oauth2common.ts b/core/packages/google-auth-library-nodejs/test/test.oauth2common.ts index b99f1f5eb0ae..c4d3226928f8 100644 --- a/core/packages/google-auth-library-nodejs/test/test.oauth2common.ts +++ b/core/packages/google-auth-library-nodejs/test/test.oauth2common.ts @@ -501,7 +501,7 @@ describe('getErrorFromOAuthErrorResponse', () => { assert.strictEqual(actualError.stack, expectedError.stack); }); - it('should keep the copied stack writable and non-enumerable', () => { + it('should keep the copied stack writable, configurable and non-enumerable', () => { const originalError = new Error('Original error message'); const resp = { error: 'invalid_grant', @@ -512,6 +512,7 @@ describe('getErrorFromOAuthErrorResponse', () => { const descriptor = Object.getOwnPropertyDescriptor(actualError, 'stack'); assert.strictEqual(descriptor?.writable, true); assert.strictEqual(descriptor?.enumerable, false); + assert.strictEqual(descriptor?.configurable, true); // Consumers append causal context to error.stack; in strict mode that // throws a TypeError when the property is read-only. assert.doesNotThrow(() => {